429 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-30 02:43:31 +00:00
1 parent 472c19bceb
commit 963cce81a7
566 files changed
+50311 -47616

No files matched your search

@@ -8,6 +8,7 @@ from datetime import timedelta
from typing import Any
import voluptuous as vol
from homeassistant.auth import EVENT_USER_REMOVED
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
@@ -51,6 +52,7 @@ from .const import (
CONF_INSTALL_ASSIST_SENTENCES,
CONF_OBJECT,
CONF_PANEL_ENABLED,
CONF_SHOPPING_LIST_ENTITY,
CONF_TASKS,
CONF_WEEKLY_DIGEST_ENABLED,
DEFAULT_PANEL_ENABLED,
@@ -141,6 +143,7 @@ SERVICE_COMPLETE_SCHEMA = vol.Schema(
# #133: when the maintenance was actually performed (backfill a past
# completion). Future values are rejected by the coordinator.
vol.Optional("completed_at"): cv.datetime,
vol.Optional("via_tag_scan"): cv.boolean,
}
)
@@ -422,6 +425,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
await doc_store.async_load()
hass.data[DOMAIN][DOCUMENT_STORE_KEY] = doc_store
# Authenticated upload + serve endpoints for document blobs (the blobs live
# under /config, so they must never be exposed via an unauthenticated path).
from .views import async_register_document_views
@@ -478,6 +482,9 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
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 _handle_reset(call: ServiceCall) -> None:
@@ -765,7 +772,14 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
try:
if action_type == "complete":
_LOGGER.info("Completing task %s via notification action", task_id)
await runtime_data.coordinator.complete_maintenance(task_id=task_id, unattended=True)
await runtime_data.coordinator.complete_maintenance(
task_id=task_id,
unattended=True,
# mobile_app fires the action with the registration's
# user in the context - credit them (#128 attribution,
# feeds least_completed rotation); bug audit 2026-08-29.
completed_by=event.context.user_id if event.context else None,
)
elif action_type == "skip":
_LOGGER.info("Skipping task %s via notification action", task_id)
await runtime_data.coordinator.skip_maintenance(task_id=task_id, reason="Skipped from notification")
@@ -835,6 +849,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
completed_by=user_id,
notes="Completed via NFC tag",
unattended=True,
tag_verified=True,
)
except ServiceValidationError as err:
# A tag tap cannot capture a cost or a photo. Log it
@@ -862,7 +877,10 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
# (DeviceInfo.suggested_area only fires once, on first device creation).
@callback
def _object_entries_for_device(
hass: HomeAssistant, device: dr.DeviceEntry
# object: HA 2026.9 registry lookups return DeviceEntry |
# ChildDeviceEntry (a union 2026.7 cannot import); the body reads
# attributes via getattr already.
hass: HomeAssistant, device: object
) -> list[ConfigEntry]:
"""Our object entries that live on this device.
@@ -894,7 +912,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
for ce in hass.config_entries.async_entries(DOMAIN):
if ce.entry_id in seen or ce.unique_id == GLOBAL_UNIQUE_ID:
continue
if (ce.data.get(CONF_OBJECT) or {}).get("ha_device_id") == device.id:
if (ce.data.get(CONF_OBJECT) or {}).get("ha_device_id") == getattr(device, "id", None):
found.append(ce)
return found
@@ -940,6 +958,21 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
from .helpers.entity_rename import rewrite_store, rewrite_tasks
# v2.67: the global shopping-list target is an entity reference too.
gentry = next(
(e for e in hass.config_entries.async_entries(DOMAIN) if e.unique_id == GLOBAL_UNIQUE_ID),
None,
)
if gentry is not None and gentry.options.get(CONF_SHOPPING_LIST_ENTITY) == old_eid:
hass.config_entries.async_update_entry(
gentry, options={**gentry.options, CONF_SHOPPING_LIST_ENTITY: new_eid}
)
from .shopping_sync import SHOPPING_SYNC_KEY
sync = hass.data.get(DOMAIN, {}).get(SHOPPING_SYNC_KEY)
if sync is not None:
await sync.async_handle_rename(old_eid, new_eid)
for ce in hass.config_entries.async_entries(DOMAIN):
if ce.unique_id == GLOBAL_UNIQUE_ID:
continue
@@ -984,6 +1017,20 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
unsub_entity = hass.bus.async_listen(er.EVENT_ENTITY_REGISTRY_UPDATED, _on_entity_registry_update)
# A deleted HA user used to be reconciled only at the next boot: the
# least_completed rotation kept assigning the ghost meanwhile and the
# admin-panel allowlist kept the stale id (bug audit 2026-08-29).
async def _on_user_removed(_event: Event) -> None:
await _check_task_responsible_user_orphans(hass)
gentry = next(
(e for e in hass.config_entries.async_entries(DOMAIN) if e.unique_id == GLOBAL_UNIQUE_ID),
None,
)
if gentry is not None:
await _check_admin_panel_user_orphans(hass, gentry)
unsub_user_removed = hass.bus.async_listen(EVENT_USER_REMOVED, _on_user_removed)
# v2.10.0: daily archive / auto-delete retention sweep (completed one-offs).
# The pure decision logic + the sweep live in helpers/retention; this just
# paces it once a day. First fire is +24h, which is plenty for a day-granular
@@ -1003,6 +1050,16 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
await async_maybe_send_lead_reminders(hass)
unsub_digest = async_track_time_change(hass, _weekly_digest_tick, hour=8, minute=0, second=0)
# The 08:00 tick drops lead reminders inside quiet hours; a quiet window
# ending after 08:00 therefore silenced them for good (bug audit
# 2026-08-29). A second, reminder-only pass at noon catches those - the
# per-day dedup inside makes it a no-op when 08:00 already delivered.
async def _lead_reminder_retry(now: Any) -> None:
await async_maybe_send_lead_reminders(hass)
unsub_lead_retry = async_track_time_change(hass, _lead_reminder_retry, hour=12, minute=0, second=0)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, lambda _e: unsub_lead_retry())
# async_track_time_change has no cancel_on_shutdown, so cancel it explicitly
# on HA stop too — otherwise the daily timer lingers past shutdown when the
# entries aren't formally unloaded (e.g. test teardown).
@@ -1010,6 +1067,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
# Store unsub callbacks so they can be cleaned up when domain is unloaded
hass.data[DOMAIN][EVENT_UNSUBS_KEY] = [
unsub_user_removed,
unsub_notification,
unsub_tag,
unsub_action,
@@ -1017,6 +1075,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
unsub_entity,
unsub_retention,
unsub_digest,
unsub_lead_retry,
]
# Once HA has started (all entries loaded), flag object entries that were
@@ -1195,6 +1254,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
entry.async_on_unload(summary_coordinator.async_teardown_listeners)
await summary_coordinator.async_refresh()
# v2.67: mirror auto "buy" tasks into the user's to-do list (no-op
# until the shopping_list_entity option is set). Owned by the GLOBAL
# entry's lifecycle - created here, torn down in async_unload_entry -
# so a reload of the hub entry gets a fresh instance (the shared-
# runtime setup only runs once per boot; bug audit 2026-08-29).
from .shopping_sync import SHOPPING_SYNC_KEY, ShoppingListSync
if SHOPPING_SYNC_KEY not in hass.data[DOMAIN]:
shopping_sync = ShoppingListSync(hass)
await shopping_sync.async_setup()
hass.data[DOMAIN][SHOPPING_SYNC_KEY] = shopping_sync
# One-time migration: auto-enable advanced feature flags for existing users
options = dict(entry.options or entry.data)
if CONF_ADVANCED_ADAPTIVE not in options:
@@ -1563,6 +1634,13 @@ async def _async_global_options_updated(hass: HomeAssistant, entry: ConfigEntry)
await async_sync_assist_sentences(
hass, entry.options.get(CONF_INSTALL_ASSIST_SENTENCES, False)
)
# v2.67: the shopping-list target may have changed (set / cleared / swapped).
# This listener does NOT reload the entry, so the sync must be nudged
# explicitly - it re-reads the option, re-arms its state listener and
# pulls its rows out of a list that is no longer the target.
from .shopping_sync import schedule_resync
schedule_resync(hass)
_ORPHAN_ISSUE_PREFIX = "orphan_admin_panel_user_"
@@ -1693,6 +1771,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: MaintenanceSupporterCon
if entry.unique_id == GLOBAL_UNIQUE_ID:
# Unregister panel when global entry is unloaded
await async_unregister_panel(hass)
from .shopping_sync import SHOPPING_SYNC_KEY
shopping_sync = hass.data.get(DOMAIN, {}).pop(SHOPPING_SYNC_KEY, None)
if shopping_sync is not None:
shopping_sync.async_teardown()
# Flush a pending debounced store save BEFORE tearing down — belt and
# suspenders next to the store cache: disk is current the moment the entry
@@ -1773,6 +1856,10 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
from homeassistant.helpers.dispatcher import async_dispatcher_send
async_dispatcher_send(hass, SIGNAL_OBJECT_ENTRY_REMOVED, entry.entry_id)
# Its buy-task rows in the shopping list are orphans now (bug audit 2026-08-29).
from .shopping_sync import schedule_resync as _schedule_shopping_resync
_schedule_shopping_resync(hass)
async def async_remove_config_entry_device(
@@ -38,5 +38,9 @@
"stock_loc": " (místo uložení: {loc})",
"stock_low": " — na úrovni objednacího limitu nebo pod ním",
"stock_untracked": "{part} — u tohoto dílu sklad nesleduju.",
"part_not_found": "Nenašel jsem náhradní díl, který by odpovídal '{name}'."
"part_not_found": "Nenašel jsem náhradní díl, který by odpovídal '{name}'.",
"st_phase": " — další krok: {phase}",
"st_priority": ", vysoká priorita",
"completed_phase": "'{task}' na {object} dokončeno — krok: {phase}. Příště: {next}.",
"guide_phase": "Aktuální krok: {phase} (krok {index} z {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (opbevaringssted {loc})",
"stock_low": " — på eller under genbestillingsgrænsen",
"stock_untracked": "Der føres ikke lager for {part}.",
"part_not_found": "Jeg kunne ikke finde en reservedel, der passer på '{name}'."
"part_not_found": "Jeg kunne ikke finde en reservedel, der passer på '{name}'.",
"st_phase": " — næste trin: {phase}",
"st_priority": ", høj prioritet",
"completed_phase": "'{task}' på {object} udført — trin: {phase}. Næste gang: {next}.",
"guide_phase": "Aktuelt trin: {phase} (trin {index} af {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (Lagerort: {loc})",
"stock_low": " — an oder unter der Nachbestellgrenze",
"stock_untracked": "Für {part} wird kein Bestand geführt.",
"part_not_found": "Ich habe kein Ersatzteil zu '{name}' gefunden."
"part_not_found": "Ich habe kein Ersatzteil zu '{name}' gefunden.",
"st_phase": " — nächster Schritt: {phase}",
"st_priority": ", hohe Priorität",
"completed_phase": "'{task}' an {object} erledigt — Schritt: {phase}. Beim nächsten Mal: {next}.",
"guide_phase": "Aktueller Schritt: {phase} (Schritt {index} von {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (stored at {loc})",
"stock_low": " — at or below the reorder threshold",
"stock_untracked": "Stock isn't tracked for {part}.",
"part_not_found": "I couldn't find a spare part matching '{name}'."
"part_not_found": "I couldn't find a spare part matching '{name}'.",
"st_phase": " — next step: {phase}",
"st_priority": ", high priority",
"completed_phase": "Completed '{task}' on {object} — step done: {phase}. Next time: {next}.",
"guide_phase": "Current step: {phase} (step {index} of {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (guardado en {loc})",
"stock_low": " — en el umbral de reposición o por debajo",
"stock_untracked": "De {part} no se lleva control de stock.",
"part_not_found": "No he encontrado ningún recambio que coincida con '{name}'."
"part_not_found": "No he encontrado ningún recambio que coincida con '{name}'.",
"st_phase": " — siguiente paso: {phase}",
"st_priority": ", prioridad alta",
"completed_phase": "'{task}' en {object} completado — paso: {phase}. La próxima vez: {next}.",
"guide_phase": "Paso actual: {phase} (paso {index} de {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (säilytyspaikka {loc})",
"stock_low": " — tilausrajalla tai sen alapuolella",
"stock_untracked": "Varastosaldoa ei seurata varaosalle {part}.",
"part_not_found": "En löytänyt varaosaa, joka vastaisi hakua '{name}'."
"part_not_found": "En löytänyt varaosaa, joka vastaisi hakua '{name}'.",
"st_phase": " — seuraava vaihe: {phase}",
"st_priority": ", korkea prioriteetti",
"completed_phase": "'{task}' ({object}) valmis — vaihe: {phase}. Ensi kerralla: {next}.",
"guide_phase": "Nykyinen vaihe: {phase} (vaihe {index}/{count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (rangé à {loc})",
"stock_low": " — au niveau ou en dessous du seuil de réapprovisionnement",
"stock_untracked": "Le stock n'est pas suivi pour {part}.",
"part_not_found": "Je n'ai pas trouvé de pièce détachée qui corresponde à '{name}'."
"part_not_found": "Je n'ai pas trouvé de pièce détachée qui corresponde à '{name}'.",
"st_phase": " — prochaine étape : {phase}",
"st_priority": ", priorité haute",
"completed_phase": "« {task} » sur {object} terminé — étape : {phase}. La prochaine fois : {next}.",
"guide_phase": "Étape actuelle : {phase} (étape {index} sur {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (रखने की जगह: {loc})",
"stock_low": " — दोबारा ऑर्डर करने की सीमा पर या उससे नीचे",
"stock_untracked": "{part} के लिए स्टॉक ट्रैक नहीं किया जाता।",
"part_not_found": "'{name}' से मिलता-जुलता कोई स्पेयर पार्ट नहीं मिला।"
"part_not_found": "'{name}' से मिलता-जुलता कोई स्पेयर पार्ट नहीं मिला।",
"st_phase": " — अगला चरण: {phase}",
"st_priority": ", उच्च प्राथमिकता",
"completed_phase": "{object} पर '{task}' पूर्ण — चरण: {phase}। अगली बार: {next}।",
"guide_phase": "वर्तमान चरण: {phase} ({count} में से चरण {index})।"
}
@@ -38,5 +38,9 @@
"stock_loc": " (tárolási hely: {loc})",
"stock_low": " — az újrarendelési küszöbön vagy alatta",
"stock_untracked": "Ehhez nincs készletnyilvántartás: {part}.",
"part_not_found": "Nem találtam ehhez illő alkatrészt: '{name}'."
"part_not_found": "Nem találtam ehhez illő alkatrészt: '{name}'.",
"st_phase": " — következő lépés: {phase}",
"st_priority": ", magas prioritás",
"completed_phase": "'{task}' ({object}) elvégezve — lépés: {phase}. Legközelebb: {next}.",
"guide_phase": "Aktuális lépés: {phase} ({index}. lépés a(z) {count} közül)."
}
@@ -38,5 +38,9 @@
"stock_loc": " (conservato in {loc})",
"stock_low": " — pari o sotto la soglia di riordino",
"stock_untracked": "Per {part} non viene tenuta la giacenza.",
"part_not_found": "Non ho trovato nessun ricambio che corrisponda a '{name}'."
"part_not_found": "Non ho trovato nessun ricambio che corrisponda a '{name}'.",
"st_phase": " — prossimo passo: {phase}",
"st_priority": ", priorità alta",
"completed_phase": "'{task}' su {object} completato — passo: {phase}. La prossima volta: {next}.",
"guide_phase": "Passo attuale: {phase} (passo {index} di {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": "(保管場所は{loc})",
"stock_low": "。在庫は発注の目安以下です",
"stock_untracked": "{part}の在庫は管理していません。",
"part_not_found": "「{name}」に一致する交換部品は見つかりませんでした。"
"part_not_found": "「{name}」に一致する交換部品は見つかりませんでした。",
"st_phase": "(次のステップ: {phase})",
"st_priority": "、優先度高",
"completed_phase": "{object}の「{task}」を完了しました(ステップ: {phase})。次回は: {next}。",
"guide_phase": "現在のステップ: {phase}({count}ステップ中{index}番目)。"
}
@@ -38,5 +38,9 @@
"stock_loc": " (보관 위치: {loc})",
"stock_low": ". 재주문 기준 이하예요",
"stock_untracked": "{part} 재고는 관리하지 않아요.",
"part_not_found": "'{name}'에 맞는 예비 부품을 찾지 못했어요."
"part_not_found": "'{name}'에 맞는 예비 부품을 찾지 못했어요.",
"st_phase": " — 다음 단계: {phase}",
"st_priority": ", 높은 우선순위",
"completed_phase": "{object}의 '{task}' 완료 — 단계: {phase}. 다음번: {next}.",
"guide_phase": "현재 단계: {phase} ({count}단계 중 {index}단계)."
}
@@ -38,5 +38,9 @@
"stock_loc": " (oppbevaringssted {loc})",
"stock_low": " — på eller under bestillingsgrensen",
"stock_untracked": "Det føres ikke lager for {part}.",
"part_not_found": "Jeg fant ingen reservedel som passer med '{name}'."
"part_not_found": "Jeg fant ingen reservedel som passer med '{name}'.",
"st_phase": " — neste trinn: {phase}",
"st_priority": ", høy prioritet",
"completed_phase": "'{task}' på {object} fullført — trinn: {phase}. Neste gang: {next}.",
"guide_phase": "Gjeldende trinn: {phase} (trinn {index} av {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (ligt in {loc})",
"stock_low": " — op of onder de bestelgrens",
"stock_untracked": "Van {part} wordt de voorraad niet bijgehouden.",
"part_not_found": "Ik kon geen onderdeel vinden dat past bij '{name}'."
"part_not_found": "Ik kon geen onderdeel vinden dat past bij '{name}'.",
"st_phase": " — volgende stap: {phase}",
"st_priority": ", hoge prioriteit",
"completed_phase": "'{task}' op {object} afgerond — stap: {phase}. Volgende keer: {next}.",
"guide_phase": "Huidige stap: {phase} (stap {index} van {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (miejsce przechowywania: {loc})",
"stock_low": " — na poziomie progu zamówienia lub poniżej",
"stock_untracked": "{part} — nie prowadzę stanu magazynowego dla tej części.",
"part_not_found": "Nie znalazłem części zamiennej pasującej do '{name}'."
"part_not_found": "Nie znalazłem części zamiennej pasującej do '{name}'.",
"st_phase": " — następny krok: {phase}",
"st_priority": ", wysoki priorytet",
"completed_phase": "Ukończono '{task}' ({object}) — krok: {phase}. Następnym razem: {next}.",
"guide_phase": "Bieżący krok: {phase} (krok {index} z {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (local: {loc})",
"stock_low": " — no limite de reposição ou abaixo dele",
"stock_untracked": "Não há controle de estoque para {part}.",
"part_not_found": "Não encontrei nenhuma peça de reposição que corresponda a '{name}'."
"part_not_found": "Não encontrei nenhuma peça de reposição que corresponda a '{name}'.",
"st_phase": " — próximo passo: {phase}",
"st_priority": ", prioridade alta",
"completed_phase": "'{task}' em {object} concluído — passo: {phase}. Na próxima vez: {next}.",
"guide_phase": "Passo atual: {phase} (passo {index} de {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (guardado em {loc})",
"stock_low": " — no limite de reposição ou abaixo",
"stock_untracked": "O stock de {part} não é acompanhado.",
"part_not_found": "Não encontrei nenhuma peça que corresponda a '{name}'."
"part_not_found": "Não encontrei nenhuma peça que corresponda a '{name}'.",
"st_phase": " — próximo passo: {phase}",
"st_priority": ", prioridade alta",
"completed_phase": "'{task}' em {object} concluído — passo: {phase}. Da próxima vez: {next}.",
"guide_phase": "Passo atual: {phase} (passo {index} de {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (место хранения: {loc})",
"stock_low": " — на уровне порога дозаказа или ниже",
"stock_untracked": "{part} — запас по этой детали не отслеживается.",
"part_not_found": "Я не нашёл запчасть, подходящую под '{name}'."
"part_not_found": "Я не нашёл запчасть, подходящую под '{name}'.",
"st_phase": " — следующий шаг: {phase}",
"st_priority": ", высокий приоритет",
"completed_phase": "«{task}» ({object}) выполнено — шаг: {phase}. В следующий раз: {next}.",
"guide_phase": "Текущий шаг: {phase} (шаг {index} из {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (förvaringsplats {loc})",
"stock_low": " — vid eller under beställningsgränsen",
"stock_untracked": "Lagersaldot följs inte för {part}.",
"part_not_found": "Jag hittade ingen reservdel som matchar '{name}'."
"part_not_found": "Jag hittade ingen reservdel som matchar '{name}'.",
"st_phase": " — nästa steg: {phase}",
"st_priority": ", hög prioritet",
"completed_phase": "'{task}' på {object} slutfört — steg: {phase}. Nästa gång: {next}.",
"guide_phase": "Aktuellt steg: {phase} (steg {index} av {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": " (saklama yeri: {loc})",
"stock_low": " — yeniden sipariş sınırında ya da altında",
"stock_untracked": "{part} için stok takibi yapılmıyor.",
"part_not_found": "'{name}' ile eşleşen bir yedek parça bulamadım."
"part_not_found": "'{name}' ile eşleşen bir yedek parça bulamadım.",
"st_phase": " — sonraki adım: {phase}",
"st_priority": ", yüksek öncelik",
"completed_phase": "{object} üzerindeki '{task}' tamamlandı — adım: {phase}. Bir dahaki sefere: {next}.",
"guide_phase": "Geçerli adım: {phase} ({count} adımdan {index}.)."
}
@@ -38,5 +38,9 @@
"stock_loc": " (місце зберігання: {loc})",
"stock_low": " — на рівні порога дозамовлення або нижче",
"stock_untracked": "{part} — запас цієї деталі не відстежується.",
"part_not_found": "Я не знайшов запчастину, яка відповідає '{name}'."
"part_not_found": "Я не знайшов запчастину, яка відповідає '{name}'.",
"st_phase": " — наступний крок: {phase}",
"st_priority": ", високий пріоритет",
"completed_phase": "«{task}» ({object}) виконано — крок: {phase}. Наступного разу: {next}.",
"guide_phase": "Поточний крок: {phase} (крок {index} з {count})."
}
@@ -38,5 +38,9 @@
"stock_loc": "(存放在{loc})",
"stock_low": ",已达到或低于补货下限",
"stock_untracked": "{part}的库存没有记录。",
"part_not_found": "我没有找到和“{name}”对得上的备件。"
"part_not_found": "我没有找到和“{name}”对得上的备件。",
"st_phase": "——下一步:{phase}",
"st_priority": ",高优先级",
"completed_phase": "已完成{object}的“{task}”——步骤:{phase}。下次:{next}。",
"guide_phase": "当前步骤:{phase}(第 {index} 步,共 {count} 步)。"
}
@@ -117,7 +117,10 @@ class MaintenanceActionButton(MaintenanceEntity, ButtonEntity):
raise HomeAssistantError(f"Task {self._task_id} no longer exists")
if self._action == "complete":
await self.coordinator.complete_maintenance(
self._task_id, notes="Completed from dashboard button", unattended=True
self._task_id,
notes="Completed from dashboard button",
unattended=True,
completed_by=self._context.user_id if self._context else None,
)
elif self._action == "skip":
await self.coordinator.skip_maintenance(self._task_id, reason="Skipped from dashboard button")
@@ -129,7 +129,10 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con
{
vol.Optional(CONF_DEFAULT_WARNING_DAYS, default=DEFAULT_WARNING_DAYS): selector.NumberSelector(
selector.NumberSelectorConfig(
min=1, max=WARNING_DAYS_RANGE[1], step=1, mode=selector.NumberSelectorMode.BOX
min=WARNING_DAYS_RANGE[0],
max=WARNING_DAYS_RANGE[1],
step=1,
mode=selector.NumberSelectorMode.BOX,
)
),
vol.Optional(CONF_NOTIFICATIONS_ENABLED, default=False): selector.BooleanSelector(),
@@ -54,6 +54,7 @@ from .const import (
CONF_QUIET_HOURS_ENABLED,
CONF_QUIET_HOURS_END,
CONF_QUIET_HOURS_START,
CONF_SHOPPING_LIST_ENTITY,
CONF_SNOOZE_DURATION_HOURS,
DEFAULT_BUDGET_CURRENCY,
DEFAULT_MAX_NOTIFICATIONS_PER_DAY,
@@ -576,6 +577,14 @@ class GlobalOptionsFlow(OptionsFlow):
else:
user_input[CONF_NOTIFY_SERVICE] = normalized
# Shopping-list target: a cleared EntitySelector omits the key —
# normalise to "" so clearing actually turns the sync off. Format
# only (todo.*): the list's integration may load later.
raw_list = (user_input.get(CONF_SHOPPING_LIST_ENTITY) or "").strip()
if raw_list and not raw_list.startswith("todo."):
errors[CONF_SHOPPING_LIST_ENTITY] = "invalid_shopping_list_entity"
user_input[CONF_SHOPPING_LIST_ENTITY] = raw_list
# Trim + cap the optional sidebar title; blank clears the override
# (panel falls back to the default "Maintenance").
raw_title = user_input.get(CONF_PANEL_TITLE)
@@ -631,6 +640,12 @@ class GlobalOptionsFlow(OptionsFlow):
custom_value=True,
)
),
# v2.67: mirror auto "buy" tasks into this to-do list
# (blank = off). suggested_value keeps the field clearable.
vol.Optional(
CONF_SHOPPING_LIST_ENTITY,
description={"suggested_value": current.get(CONF_SHOPPING_LIST_ENTITY, "")},
): selector.EntitySelector(selector.EntitySelectorConfig(domain="todo")),
vol.Optional(
CONF_PANEL_ENABLED,
default=current.get(CONF_PANEL_ENABLED, DEFAULT_PANEL_ENABLED),
@@ -264,6 +264,8 @@ class TaskCrudMixin:
updated_task[CONF_TASK_NFC_TAG] = nfc_val
else:
updated_task.pop(CONF_TASK_NFC_TAG, None)
# Proof of presence: completion only via NFC/QR scan.
updated_task["require_tag_scan"] = bool(user_input.get("require_tag_scan"))
# v2.20 (#83): reading unit — clear by submitting "".
if CONF_TASK_READING_UNIT in user_input:
ru = (user_input.get(CONF_TASK_READING_UNIT) or "").strip()
@@ -483,6 +485,7 @@ class TaskCrudMixin:
default=", ".join(task.get("labels", [])),
): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
nfc_tag_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
vol.Optional("require_tag_scan", default=bool(task.get("require_tag_scan"))): selector.BooleanSelector(),
reading_unit_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
vol.Optional("go_back", default=False): selector.BooleanSelector(),
}
@@ -111,6 +111,9 @@ SOUTHERN_SEASONS: dict[str, list[int]] = {
CONF_DEFAULT_WARNING_DAYS = "default_warning_days"
CONF_NOTIFICATIONS_ENABLED = "notifications_enabled"
CONF_NOTIFY_SERVICE = "notify_service"
# v2.67: mirror auto "buy" tasks into a user-picked HA to-do list (todo.*).
# Empty = feature off. Instance-bound → excluded from settings export.
CONF_SHOPPING_LIST_ENTITY = "shopping_list_entity"
CONF_PANEL_ENABLED = "panel_enabled"
# v2.10.4 (#69 follow-up): the sidebar panel is the integration's hub — the
# auto-dashboard's "Open Maintenance panel" button, QR codes, and notifications
@@ -450,6 +453,20 @@ DEFAULT_DEGRADATION_LOOKBACK_DAYS = 30 # Days of recorder data for slope comput
DEFAULT_DEGRADATION_MIN_POINTS = 10 # Min hourly data points to compute regression
DEFAULT_DEGRADATION_SIGNIFICANCE = 0.05 # Min |slope|/mean ratio for rising/falling
# --- Cycle-aware degradation (ET-4800 ink finding, 2026-08) ---
# Consumption sensors are sawtooths: the value drifts toward the threshold and
# JUMPS back on a refill/service. A regression across that jump reports the
# refill as a rising trend. The analysis therefore splits the series into
# service cycles at significant against-the-drift jumps, regresses the CURRENT
# cycle, and LEARNS the typical rate from prior cycles (median, blended in
# while the current cycle is still thin).
DEGRADATION_CYCLE_LOOKBACK_DAYS = 180 # window for learning prior cycles
DEGRADATION_CYCLE_JUMP_FRACTION = 0.2 # jump ≥ this share of the observed range = boundary
DEGRADATION_BLEND_HALF_POINTS = 48 # current-cycle points at which current/history blend 50/50
DEGRADATION_MIN_CYCLE_HOURS = 24 # prior cycles shorter than this teach nothing
DEGRADATION_CYCLE_NOISE_SIGMAS = 8 # a boundary must also exceed this many robust sigmas of hourly wobble
DEGRADATION_CYCLE_SETTLE_POINTS = 24 # a jump that snaps back within this many points is modulation
# --- Environmental Adjustment Defaults (Phase 3) ---
DEFAULT_ENVIRONMENTAL_LOOKBACK_DAYS = 90 # Days of env data for correlation
DEFAULT_ENVIRONMENTAL_CORRELATION_MIN = 0.3 # Min |r| to apply adjustment
@@ -193,6 +193,9 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._recent_manual_completions = {
tid: ts for tid, ts in self._recent_manual_completions.items() if now_mono - ts < MANUAL_COMPLETION_DEDUP_SECONDS
}
self._recent_backfills = {
key: ts for key, ts in self._recent_backfills.items() if now_mono - ts < MANUAL_COMPLETION_DEDUP_SECONDS
}
result: dict[str, Any] = {
CONF_OBJECT: obj.to_dict(),
@@ -357,6 +360,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
task_result["_degradation_trend"] = deg.trend
task_result["_degradation_r_squared"] = deg.r_squared
task_result["_degradation_data_points"] = deg.data_points
task_result["_prediction_cycles"] = deg.cycles_learned
# Threshold prediction
if prediction.threshold_prediction:
@@ -716,6 +720,15 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
_vac = get_vacation_state(self.hass)
notifiable = [row for row in notifiable if not _vac.is_silent_for(row[0])]
# A status the user switched off, or a task they snoozed, must neither
# count toward the bundle threshold nor ride inside the bundle - the
# per-task path checks both, the bundle path did not (bug audit
# 2026-08-29).
notifiable = [
row
for row in notifiable
if nm._is_status_enabled(row[2]) and not nm._is_snoozed(f"{self.entry.entry_id}_{row[0]}_{row[2]}")
]
# v2.26 notification routing: a saved-view scope ("only notify about
# view X") drops tasks the view's label/user filters don't match —
@@ -940,6 +953,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
auto: bool = False,
unattended: bool = False,
completed_at: datetime | None = None,
tag_verified: bool = False,
) -> None:
"""Mark a task as completed and persist.
@@ -1012,6 +1026,55 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
},
)
# Proof of presence: a task flagged require_tag_scan only completes
# from a surface that proves someone stood at the thing — an NFC tap
# or the QR quick-complete. Checked at THIS choke point so every
# other surface (panel, card, to-do, voice, notification button)
# gets the same refusal. Automatic completions are exempt (a
# recovered trigger has nobody to send to the machine), and
# automations may assert the scan via the service's `via_tag_scan`.
if not auto and not tag_verified and merged[task_id].get("require_tag_scan"):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="tag_scan_required",
translation_placeholders={
"task_name": str(merged[task_id].get("name", task_id)),
},
)
# Inert tasks (bug audit 2026-08-29): the auto path refuses archived /
# disabled tasks and paused objects (see async_auto_complete_on_recovery),
# but the manual path let an NFC sticker on a retired machine, a stale
# panel or a voice command record a completion on a task nobody can
# see any more - rotation advanced, parts consumed. Same gate here.
if not auto:
_td = merged[task_id]
if (
_td.get("archived_at") is not None
or _td.get("enabled") is False
or self.entry.data.get(CONF_OBJECT, {}).get("paused_at") is not None
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="task_inactive",
translation_placeholders={"task_name": str(_td.get("name", task_id))},
)
# Completion window (earliest_completion_days) - at the choke point like
# every other rule. The WS / to-do / voice surfaces pre-check it for a
# friendly code, but the service, the dashboard button, an NFC tap and a
# notification button reached this method with no gate at all (bug audit
# 2026-08-29). A completion dated on a PAST day is a history correction,
# not early work, and bypasses the window exactly like the WS pre-check.
if not auto:
_past_dated = completed_at is not None and completed_at.date() < dt_util.now().date()
if not _past_dated and not MaintenanceTask.from_dict(merged[task_id]).can_complete_now:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="too_early",
translation_placeholders={"task_name": str(merged[task_id].get("name", task_id))},
)
# Household double-complete guard (journey M1): two people seeing the
# same overdue task and both tapping Complete within seconds would
# record two completions — duplicated history/cost and a DOUBLE
@@ -1077,8 +1140,20 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# stay correctable through the history editor like dialog ones.
from .helpers.phases import effective_field as _phase_effective
# A pure backfill (dated before the last completion) performed no work
# in the CURRENT cycle: the automatic consumes_parts / buy-task restock
# must not run for it (bug audit 2026-08-29) - an explicit used_parts
# selection is still honoured. Mirrors MaintenanceTask.complete's
# is_latest split (ISO-date string compare).
_is_backfill = (
completed_at is not None
and bool(task.last_performed)
and effective_ts.date().isoformat() < str(task.last_performed)
)
record_links = (
used_parts if used_parts is not None else (_phase_effective(merged[task_id], "consumes_parts") or [])
used_parts
if used_parts is not None
else ([] if _is_backfill else (_phase_effective(merged[task_id], "consumes_parts") or []))
)
enriched_used: list[dict[str, Any]] | None = None
if used_parts is not None or record_links:
@@ -1144,10 +1219,24 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
task.adaptive_config["hemisphere"] = "south" if (self.hass.config.latitude or 0) < 0 else "north"
task.adaptive_config["_current_month"] = effective_ts.month
task.adaptive_config["_current_date"] = effective_ts.date().isoformat()
updated_config = analyzer.update_on_completion(task.adaptive_config, actual_interval, feedback)
task.adaptive_config = updated_config
try:
updated_config = analyzer.update_on_completion(task.adaptive_config, actual_interval, feedback)
task.adaptive_config = updated_config
except (TypeError, ValueError, ZeroDivisionError): # hand-edited / imported adaptive_config
_LOGGER.warning("Adaptive learning skipped for %s: malformed adaptive_config", task_id)
await self._persist_and_signal_task_change(task_id, task)
try:
await self._persist_and_signal_task_change(task_id, task)
except Exception:
# The completion did not land on disk - release the double-tap
# guard stamped above, or the user's retry within 30 s would be
# swallowed as a duplicate of a completion that never happened
# (bug audit 2026-08-29).
if completed_at is None and not auto:
self._recent_manual_completions.pop(task_id, None)
elif completed_at is not None:
self._recent_backfills.pop((task_id, completed_at.isoformat()), None)
raise
# Shared-task rotation: advance_rotation() (inside task.complete)
# mutates responsible_user_id, which is a STATIC config field — the
@@ -1172,16 +1261,24 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
from .parts_runtime import async_handle_completion_parts
try:
await async_handle_completion_parts(
self.hass,
self.entry,
merged[task_id],
restock_quantity=restock_quantity,
used_parts=used_parts,
)
if is_latest or used_parts is not None:
await async_handle_completion_parts(
self.hass,
self.entry,
merged[task_id],
restock_quantity=restock_quantity,
used_parts=used_parts,
)
except Exception:
_LOGGER.exception("Part consumption failed for task %s", task_id)
# A status configured to notify ONCE (interval 0) stays silenced by the
# _SENT_ONCE sentinel until something clears it; nothing did after a
# completion, so a sensor task that re-triggered weeks later was never
# announced again until HA restarted (bug audit 2026-08-29).
if is_latest:
self._clear_notification_state(task_id)
_LOGGER.debug("Maintenance completed: %s on %s", task.name, self.maintenance_object.name)
# Fire event after persistence — power users wire HA automations on
@@ -1276,6 +1373,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# A reset starts a new cycle — a following completion is a NEW
# real-world action, never a double-tap of the previous one.
self._recent_manual_completions.pop(task_id, None)
self._clear_notification_state(task_id)
merged = self._get_merged_tasks_data()
if task_id not in merged:
_LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title)
@@ -1310,6 +1408,18 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
await self._persist_and_signal_task_change(task_id, task)
_LOGGER.debug("Occurrence postponed to %s: %s on %s", until, task.name, self.maintenance_object.name)
def _clear_notification_state(self, task_id: str) -> None:
"""Forget the per-status notification bookkeeping for a task.
The notification manager lives on the shared runtime; it may be absent
in tests or during teardown - then there is nothing to clear.
"""
from .const import NOTIFICATION_MANAGER_KEY
nm = self.hass.data.get(DOMAIN, {}).get(NOTIFICATION_MANAGER_KEY)
if nm is not None:
nm.clear_task_state(self.entry.entry_id, task_id)
async def skip_maintenance(
self,
task_id: str,
@@ -1320,6 +1430,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Same as reset: skipping restarts the cycle — clear the double-tap
# window so a follow-up completion counts.
self._recent_manual_completions.pop(task_id, None)
self._clear_notification_state(task_id)
merged = self._get_merged_tasks_data()
if task_id not in merged:
_LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title)
@@ -58,7 +58,12 @@ class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]):
coordinator.hass, device_id, own_entry_id=coordinator.entry.entry_id
)
if resolved is not None:
self._linked_device = dr.async_get(coordinator.hass).async_get(resolved)
# Any hop: HA 2026.9 lookups return DeviceEntry |
# ChildDeviceEntry (both attach to entities the same way);
# 2026.7 returns the plain entry — a cast would be redundant
# on one version and required on the other.
resolved_device: Any = dr.async_get(coordinator.hass).async_get(resolved)
self._linked_device = resolved_device
if self._linked_device is not None:
self.device_entry = self._linked_device
@@ -42,7 +42,7 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
update_interval=None,
)
self._unsubs: list[CALLBACK_TYPE] = []
self._attached: dict[str, CALLBACK_TYPE] = {}
self._attached: dict[str, tuple[Any, CALLBACK_TYPE]] = {}
async def _async_update_data(self) -> dict[str, Any]:
"""Recompute counts from the shared aggregator (single source)."""
@@ -62,12 +62,22 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._unsubs.append(self.hass.bus.async_listen(EVENT_TRIGGER_DEACTIVATED, self._on_event))
def _attach_entry(self, entry_id: str) -> None:
"""Register a listener on a single object coordinator (once)."""
if entry_id in self._attached:
return
"""Register a listener on a single object coordinator.
A reload swaps the coordinator and re-announces the entry; the old
"once" guard left the summary sensors listening to the dead one (bug
audit 2026-08-29), so re-attach whenever the instance changed.
"""
rd = get_runtime_data(self.hass, entry_id)
if rd and rd.coordinator:
self._attached[entry_id] = rd.coordinator.async_add_listener(self._schedule)
coordinator = rd.coordinator if rd else None
if coordinator is None:
return
previous = self._attached.get(entry_id)
if previous is not None:
if previous[0] is coordinator:
return
previous[1]()
self._attached[entry_id] = (coordinator, coordinator.async_add_listener(self._schedule))
@callback
def _on_new_entry(self, entry_id: str) -> None:
@@ -78,9 +88,9 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@callback
def _on_removed(self, entry_id: str) -> None:
"""An object entry was deleted — detach its listener and recompute."""
unsub = self._attached.pop(entry_id, None)
if unsub is not None:
unsub()
attached = self._attached.pop(entry_id, None)
if attached is not None:
attached[1]()
self._schedule()
@callback
@@ -97,7 +107,7 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Detach all listeners (called on global entry unload)."""
for unsub in self._unsubs:
unsub()
for unsub in self._attached.values():
for _coordinator, unsub in self._attached.values():
unsub()
self._unsubs.clear()
self._attached.clear()
@@ -11,6 +11,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import (
BATTERY_FLEET_EXCLUDED,
BATTERY_FLEET_INCLUDED,
BATTERY_FLEET_OBJECT_FLAG,
BATTERY_FLEET_TASK_FLAG,
BATTERY_FLEET_TRACK_SELF_CHARGING,
CONF_OBJECT,
CONF_TASKS,
DEFAULT_WARNING_DAYS,
@@ -111,6 +116,7 @@ def _build_export_object(
"documentation_url": tdata.get("documentation_url"),
"custom_icon": tdata.get("custom_icon"),
"nfc_tag_id": tdata.get("nfc_tag_id"),
"require_tag_scan": tdata.get("require_tag_scan"),
"responsible_user_id": tdata.get("responsible_user_id"),
"entity_slug": tdata.get("entity_slug"),
"adaptive_config": tdata.get("adaptive_config"),
@@ -155,6 +161,12 @@ def _build_export_object(
if trigger_config:
task["trigger_config"] = trigger_config
# Battery fleet identity: the single aggregate task is flagged so the
# detail view renders the battery section — without the marker a
# restored fleet task is a plain inspection task.
if tdata.get(BATTERY_FLEET_TASK_FLAG):
task[BATTERY_FLEET_TASK_FLAG] = True
if include_history:
task["history"] = tdata.get("history") or []
@@ -167,33 +179,48 @@ def _build_export_object(
object_id = obj_data.get("id", "")
documents = _export_documents(doc_store, object_id) if doc_store is not None and object_id else []
export_obj: dict[str, Any] = {
"name": obj_data.get("name", ""),
"area_id": obj_data.get("area_id"),
"manufacturer": obj_data.get("manufacturer"),
"model": obj_data.get("model"),
"serial_number": obj_data.get("serial_number"),
"installation_date": obj_data.get("installation_date"),
"warranty_expiry": obj_data.get("warranty_expiry"),
# Round-tripped so a JSON backup restores the full asset record
# (these were added in v1.4.0/v1.4.10 but missed here until #67).
"documentation_url": obj_data.get("documentation_url"),
"notes": obj_data.get("notes"),
# 2.19: device link / parent hierarchy. Instance-specific ids —
# meaningful when restoring on the SAME instance; dangling values
# on a foreign instance are harmless (device_info falls back).
"ha_device_id": obj_data.get("ha_device_id"),
"parent_entry_id": obj_data.get("parent_entry_id"),
# 2.20: seasonal pause (a paused pool restored in winter stays
# paused) + replace-flow lineage (instance-specific entry ids,
# same caveat as parent_entry_id above).
"paused_at": obj_data.get("paused_at"),
"paused_until": obj_data.get("paused_until"),
"predecessor_entry_id": obj_data.get("predecessor_entry_id"),
"replaced_by_entry_id": obj_data.get("replaced_by_entry_id"),
# Object-level archive marker (the tasks carry their own pair
# above). Without it an archived object came back ACTIVE after a
# restore — with every task still archived (reason "object") and
# object/unarchive refusing because the object "isn't archived".
"archived_at": obj_data.get("archived_at"),
}
# Battery fleet identity — only emitted for the fleet object so a plain
# object's export stays byte-identical to earlier versions. The importer
# mirrors these (and keeps the deterministic ``batt_<type>`` part ids).
if obj_data.get(BATTERY_FLEET_OBJECT_FLAG):
export_obj[BATTERY_FLEET_OBJECT_FLAG] = True
export_obj[BATTERY_FLEET_EXCLUDED] = list(obj_data.get(BATTERY_FLEET_EXCLUDED) or [])
export_obj[BATTERY_FLEET_INCLUDED] = list(obj_data.get(BATTERY_FLEET_INCLUDED) or [])
export_obj[BATTERY_FLEET_TRACK_SELF_CHARGING] = bool(obj_data.get(BATTERY_FLEET_TRACK_SELF_CHARGING))
return {
"entry_id": entry.entry_id,
"object": {
"name": obj_data.get("name", ""),
"area_id": obj_data.get("area_id"),
"manufacturer": obj_data.get("manufacturer"),
"model": obj_data.get("model"),
"serial_number": obj_data.get("serial_number"),
"installation_date": obj_data.get("installation_date"),
"warranty_expiry": obj_data.get("warranty_expiry"),
# Round-tripped so a JSON backup restores the full asset record
# (these were added in v1.4.0/v1.4.10 but missed here until #67).
"documentation_url": obj_data.get("documentation_url"),
"notes": obj_data.get("notes"),
# 2.19: device link / parent hierarchy. Instance-specific ids —
# meaningful when restoring on the SAME instance; dangling values
# on a foreign instance are harmless (device_info falls back).
"ha_device_id": obj_data.get("ha_device_id"),
"parent_entry_id": obj_data.get("parent_entry_id"),
# 2.20: seasonal pause (a paused pool restored in winter stays
# paused) + replace-flow lineage (instance-specific entry ids,
# same caveat as parent_entry_id above).
"paused_at": obj_data.get("paused_at"),
"paused_until": obj_data.get("paused_until"),
"predecessor_entry_id": obj_data.get("predecessor_entry_id"),
"replaced_by_entry_id": obj_data.get("replaced_by_entry_id"),
},
"object": export_obj,
"tasks": tasks,
"documents": documents,
# Spare parts: full static definition + the tracked stock (dynamic,
@@ -293,7 +320,7 @@ def export_maintenance_data(
# Global settings the export deliberately leaves behind: HA user ids are
# instance-bound (and the panel-access allowlist is security-relevant), and
# the adopted-task stash is transient re-adopt state.
_NON_PORTABLE_SETTINGS = ("admin_panel_user_ids", "adopted_task_notes")
_NON_PORTABLE_SETTINGS = ("admin_panel_user_ids", "adopted_task_notes", "shopping_list_entity")
def build_settings_export(hass: HomeAssistant) -> dict[str, Any]:
@@ -273,3 +273,27 @@ describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => {
expect(chip(el)).to.equal(null);
});
});
describe("complete-dialog require_tag_scan note", () => {
it("shows the scan hint when the task demands proof of presence", async () => {
const { hass } = createMockHass({});
const el = await fixture<MaintenanceCompleteDialog>(html`
<maintenance-complete-dialog
.hass=${hass}
.entryId=${"entry1"}
.taskId=${"task1"}
.taskName=${"Filter Wechsel"}
.lang=${"en"}
.requireTagScan=${true}
></maintenance-complete-dialog>
`);
el.open();
await el.updateComplete;
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.exist;
});
it("stays silent for ordinary tasks", async () => {
const { el } = await mount();
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.equal(null);
});
});
@@ -5,7 +5,7 @@
import { expect } from "@open-wc/testing";
import { formatDate, formatDateTime, setDateTimePrefs } from "../styles";
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined });
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined }, null);
describe("formatDate with HA profile date_format (#97)", () => {
afterEach(reset);
@@ -66,3 +66,39 @@ describe("formatDateTime with HA profile time_format (#97)", () => {
expect(out.toLowerCase()).to.match(/pm|nachm/);
});
});
describe("server country regionalizes the language default (#140)", () => {
afterEach(reset);
it('"en" + AU renders dd/mm/yyyy without any profile setting', () => {
setDateTimePrefs({ date_format: "language" }, "AU");
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
});
it('"en" + US keeps mm/dd/yyyy; "de" + AT stays dd.mm.yyyy', () => {
setDateTimePrefs({ date_format: "language" }, "US");
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
setDateTimePrefs({ date_format: "language" }, "AT");
expect(formatDate("2026-08-10", "de")).to.equal("10.08.2026");
});
it("an explicit profile format always beats the country", () => {
setDateTimePrefs({ date_format: "MDY" }, "AU");
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
setDateTimePrefs({ date_format: "YMD" }, "AU");
expect(formatDate("2026-08-10", "en")).to.equal("2026-08-10");
});
it("garbage/unset country falls back to the language mapping", () => {
setDateTimePrefs({ date_format: "language" }, "!!");
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
setDateTimePrefs({ date_format: "language" }, null);
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
});
it("country survives a later prefs update that omits it (undefined ≠ clear)", () => {
setDateTimePrefs({ date_format: "language" }, "AU");
setDateTimePrefs({ date_format: "language" });
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
});
});
@@ -114,6 +114,83 @@ describe("panel deep links (QR scan routing)", () => {
expect(dlg.taskName).to.equal("No Defaults");
});
it("quick_complete refused for missing details falls back to the dialog WITH the scan flag", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
const { el, sent } = await mountPanel(
[obj("e1", [task({ name: "Needs Notes", require_tag_scan: true })])],
{
"maintenance_supporter/task/quick_complete": () => {
throw { code: "completion_details_required", message: "notes required" };
},
},
);
await settleRaf(el);
await settleRaf(el);
await waitForOpenCompleteDialog(el);
const dlg = completeDialog(el)!;
expect(dlg.shadowRoot!.querySelector("ha-dialog"), "fallback dialog open").to.exist;
expect(dlg.taskName).to.equal("Needs Notes");
// The scan already happened: the gate is met, the note is not shown …
expect(dlg.viaTagScan).to.equal(true);
expect(dlg.requireTagScan).to.equal(true);
expect(dlg.shadowRoot!.querySelector(".scan-required-note")).to.be.null;
// … and the completion carries via_tag_scan for the backend's gate.
const buttons = [...dlg.shadowRoot!.querySelectorAll(".dialog-actions ha-button")];
(buttons[buttons.length - 1] as HTMLElement).click();
await new Promise((r) => setTimeout(r, 20));
const complete = sent.find((m) => m.type === "maintenance_supporter/task/complete");
expect(complete, "task/complete sent").to.exist;
expect(complete!.via_tag_scan).to.equal(true);
});
it("a plain complete (no scan) opens the dialog WITHOUT the scan flag", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=complete");
const { el } = await mountPanel([obj("e1", [task({ name: "Gated", require_tag_scan: true })])]);
await settleRaf(el);
await waitForOpenCompleteDialog(el);
const dlg = completeDialog(el)!;
expect(dlg.viaTagScan).to.equal(false);
expect(dlg.shadowRoot!.querySelector(".scan-required-note"), "note shown").to.exist;
});
it("any other quick_complete refusal relays the server's message", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
const { el } = await mountPanel(
[obj("e1", [task({ name: "Early" })])],
{
"maintenance_supporter/task/quick_complete": () => {
throw { code: "too_early", message: "Earliest completion is 2026-09-01" };
},
},
);
await settleRaf(el);
await settleRaf(el);
expect((el as unknown as { _toastMessage: string })._toastMessage).to.equal(
"Earliest completion is 2026-09-01",
);
const dlg = completeDialog(el);
expect(dlg?.shadowRoot?.querySelector("ha-dialog") ?? null, "no dialog").to.be.null;
});
it("a silent quick_complete success reloads the data", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
const { el, sent } = await mountPanel(
[obj("e1", [task({ name: "Quick" })])],
{ "maintenance_supporter/task/quick_complete": () => ({ success: true, via: "quick" }) },
);
await settleRaf(el);
await settleRaf(el);
// The deep link is only routed once the objects are known, so every
// objects fetch AFTER the quick_complete is the reload it triggered.
const atQuick = sent.findIndex((m) => m.type === "maintenance_supporter/task/quick_complete");
expect(atQuick, "quick_complete fired").to.be.greaterThan(-1);
const reloaded = sent
.slice(atQuick + 1)
.some((m) => m.type === "maintenance_supporter/objects");
expect(reloaded, "objects refetched after the silent complete").to.equal(true);
});
it("an unknown entry_id lands safely on the overview", async () => {
setDeepLink("entry_id=does-not-exist&task_id=t1&action=complete");
const { el } = await mountPanel([obj("e1", [task({ name: "Real" })])]);
@@ -215,6 +215,19 @@ describe("task-detail renderer", () => {
expect(host2.querySelector(".kpi-bar")).to.be.null;
});
it("history tab offers 'add past completion' routing to openComplete (#142)", () => {
let completed: MaintenanceTask | null = null;
const t2 = task();
const host = mount(t2, ctx({
activeTab: "history",
openComplete: (tk) => { completed = tk; },
}));
const btn = host.querySelector(".history-add-past-btn") as HTMLElement;
expect(btn, "add-past button").to.exist;
btn.click();
expect(completed).to.equal(t2);
});
it("object-manual row falls back to an attached manual when the URL is empty", () => {
let opened: unknown = null;
const host = mount(task(), ctx({
@@ -43,6 +43,15 @@ export class MaintenanceCompleteDialog extends LitElement {
/** #139: "2/4 · Flip blades" — which phase of the cycle this completion
* records. Purely informative; the backend advances the cursor itself. */
@property() public phaseLabel = "";
/** Proof of presence: the task completes only via NFC/QR scan — the
* dialog says so up front instead of relaying the server's refusal. */
@property({ type: Boolean }) public requireTagScan = false;
/** This open is the fallback of a QR/NFC scan (quick-complete refused for
* missing defaults/details): the completion carries `via_tag_scan` so the
* tag-scan gate counts the scan that got the user here, and the "scan
* required" note stays hidden — they just did. Set by open(); never
* survives into the next open. */
@property({ type: Boolean }) public viaTagScan = false;
@state() private _open = false;
@state() private _notes = "";
@state() private _cost = "";
@@ -66,9 +75,12 @@ export class MaintenanceCompleteDialog extends LitElement {
* prefill the dialog so nobody re-ticks what is already done. */
@property({ attribute: false }) public checklistPrefill: Record<string, boolean> = {};
public open(): void {
public open(opts: { viaTagScan?: boolean } = {}): void {
if (this._open) return;
this._open = true;
// Reset on every open — a plain open after a scan fallback must not
// keep claiming proof of presence.
this.viaTagScan = !!opts.viaTagScan;
this._notes = "";
this._cost = "";
this._duration = "";
@@ -173,6 +185,12 @@ export class MaintenanceCompleteDialog extends LitElement {
if (this._photoDocId) {
data.photo_doc_id = this._photoDocId;
}
// Scan fallback: the backend accepts via_tag_scan on task/complete so a
// require_tag_scan task can still be finished from the dialog the scan
// opened. Only ever sent when a scan actually happened.
if (this.viaTagScan) {
data.via_tag_scan = true;
}
if (this._completedAt) {
// Client-side guard mirrors the backend rule — a picked future moment
// fails fast with a localized message instead of a WS roundtrip.
@@ -290,6 +308,7 @@ export class MaintenanceCompleteDialog extends LitElement {
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${t("complete_title", L)}${this.taskName}</div>
${this.phaseLabel ? html`<div class="phase-line">${t("phase_current", L)}: ${this.phaseLabel}</div>` : nothing}
${this.requireTagScan && !this.viaTagScan ? html`<div class="scan-required-note">${t("require_tag_scan_hint", L)}</div>` : nothing}
<div class="content">
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
${this.checklist.length > 0 ? html`
@@ -477,6 +496,14 @@ export class MaintenanceCompleteDialog extends LitElement {
font-weight: 500;
padding-bottom: 12px;
}
.scan-required-note {
margin: -4px 0 12px;
padding: 8px 10px;
border-radius: 6px;
background: rgba(255, 152, 0, 0.12);
color: var(--primary-text-color);
font-size: 13px;
}
.phase-line {
margin-top: -8px;
padding-bottom: 12px;
@@ -39,6 +39,10 @@ export class MsTextfield extends LitElement {
@property() public max?: string;
@property() public pattern?: string;
@property() public helper?: string;
/** Render a <textarea> instead of an <input> (free-text notes). The
* `input` event contract is identical — target.value works for both. */
@property({ type: Boolean }) public multiline = false;
@property({ type: Number }) public rows = 3;
/** Forwards the native input's value into our `value` property and
* re-fires as a bubbling `input` event so consumers reading
@@ -56,6 +60,16 @@ export class MsTextfield extends LitElement {
return html`
<label class="field">
${this.label ? html`<span class="label">${this.label}${this.required ? html`<span class="req">*</span>` : nothing}</span>` : nothing}
${this.multiline ? html`
<textarea
.value=${this.value ?? ""}
rows=${this.rows}
?required=${this.required}
?disabled=${this.disabled}
placeholder=${this.placeholder}
@input=${this._onInput}
@change=${this._onInput}
></textarea>` : html`
<input
.value=${this.value ?? ""}
.type=${this.type}
@@ -68,7 +82,7 @@ export class MsTextfield extends LitElement {
pattern=${this.pattern ?? nothing}
@input=${this._onInput}
@change=${this._onInput}
/>
/>`}
${this.helper ? html`<span class="helper">${this.helper}</span>` : nothing}
</label>
`;
@@ -103,6 +117,21 @@ export class MsTextfield extends LitElement {
border-color: var(--primary-color);
}
input:disabled { opacity: 0.5; cursor: not-allowed; }
textarea {
padding: 8px 10px;
font-size: 14px;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
border-radius: 6px;
font-family: inherit;
width: 100%;
box-sizing: border-box;
outline: none;
resize: vertical;
}
textarea:focus { border-color: var(--primary-color); }
textarea:disabled { opacity: 0.5; cursor: not-allowed; }
.helper {
font-size: 11px;
color: var(--secondary-text-color);
@@ -217,6 +217,7 @@ export class MaintenanceObjectDialog extends LitElement {
.value=${this._notes}
@input=${(e: Event) => (this._notes = (e.target as HTMLTextAreaElement).value)}
></textarea>
<span class="md-hint">${t("notes_markdown_hint", L)}</span>
</label>
</div>
<div class="dialog-actions">
@@ -271,6 +272,9 @@ export class MaintenanceObjectDialog extends LitElement {
.textarea-field textarea:focus {
outline: none; border-color: var(--primary-color);
}
.md-hint {
font-size: 11px; color: var(--secondary-text-color); font-style: italic;
}
.parent-select {
padding: 8px 10px; font-size: 14px; font-family: inherit;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
@@ -10,6 +10,7 @@
import { LitElement, html, css, nothing } from "lit";
import { isSafeHttpUrl } from "../helpers/url";
import { renderNotesMarkdown } from "../helpers/notes-markdown";
import { property, state } from "lit/decorators.js";
import { t, STATUS_COLORS, langOf } from "../styles";
import { describeWsError } from "../ws-errors";
@@ -186,7 +187,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
? html`
<div class="notes-section">
<strong>${t("object_notes_label", L)}</strong>
<div class="notes-body">${obj.notes}</div>
<div class="notes-body">${renderNotesMarkdown(obj.notes)}</div>
</div>
`
: nothing}
@@ -294,6 +295,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
.task-name { flex: 1; font-size: 14px; }
.task-status { font-size: 11px; color: var(--secondary-text-color); text-transform: uppercase; }
.notes-body { white-space: pre-wrap; font-size: 13px; padding: 8px; background: var(--secondary-background-color); border-radius: 6px; }
.notes-body ha-markdown { white-space: normal; }
.actions { display: flex; gap: 8px; padding-top: 8px; border-top: 1px solid var(--divider-color); }
.actions .btn { flex: 1; }
.btn {
@@ -3,6 +3,7 @@
import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { live } from "lit/directives/live.js";
import type { HomeAssistant, AdvancedFeatures, BudgetStatus, HAUser } from "../types";
import { t, langOf } from "../styles";
import { signApiPath } from "../helpers/document-url";
@@ -36,6 +37,8 @@ interface SettingsResponse {
// so the picker matches the options-flow dropdown exactly. Optional for
// backward-compat with an older backend that didn't send it.
notify_targets?: string[];
/** v2.67: todo.* entity the buy-task shopping sync mirrors into ("" = off). */
shopping_list_entity?: string;
panel_enabled: boolean;
panel_title: string;
/** Opt-in copy of the shipped Assist sentences into the config dir. */
@@ -249,6 +252,11 @@ export class MaintenanceSettingsView extends LitElement {
this.dispatchEvent(new CustomEvent("settings-changed"));
} catch {
this._showToast(t("action_error", this._lang));
// The control already shows the rejected value (the browser changed it
// before we asked the server). `_settings` is unchanged, so a plain
// re-render would not touch the DOM — the selects bind their value
// through `live()` precisely so this re-render snaps them back.
this.requestUpdate();
}
}
@@ -579,15 +587,16 @@ export class MaintenanceSettingsView extends LitElement {
<h3>${t("settings_general", L)}</h3>
<label class="setting-row">
<span class="setting-label">${t("settings_default_warning", L)}</span>
<input type="number" min="1" max="365" .value=${String(g.default_warning_days)}
<input type="number" min="0" max="365" .value=${String(g.default_warning_days)}
@change=${(e: Event) => {
const v = parseInt((e.target as HTMLInputElement).value, 10);
if (v >= 1 && v <= 365) this._updateSetting("default_warning_days", v);
// 0 = no warning window (due soon only on the due date) — #145.
if (v >= 0 && v <= 365) this._updateSetting("default_warning_days", v);
}} />
</label>
<label class="setting-row">
<span class="setting-label">${t("settings_currency", L)}</span>
<select @change=${(e: Event) => this._updateSetting("budget_currency", (e.target as HTMLSelectElement).value)}>
<select .value=${live(b.currency)} @change=${(e: Event) => this._updateSetting("budget_currency", (e.target as HTMLSelectElement).value)}>
${CURRENCIES.map((c) => html`<option value=${c} ?selected=${b.currency === c}>${c}</option>`)}
</select>
</label>
@@ -652,10 +661,29 @@ export class MaintenanceSettingsView extends LitElement {
</div>
` : nothing}
` : nothing}
<label class="setting-row">
<span class="setting-label" title=${t("settings_shopping_list_help", L)}>${t("settings_shopping_list", L)}</span>
<select .value=${live(g.shopping_list_entity || "")}
@change=${(e: Event) => this._updateSetting("shopping_list_entity", (e.target as HTMLSelectElement).value)}>
<option value="" ?selected=${!g.shopping_list_entity}>${t("shopping_list_none", L)}</option>
${this._todoEntities(g.shopping_list_entity || "").map((id) => html`
<option value=${id} ?selected=${g.shopping_list_entity === id}>${id}</option>
`)}
</select>
</label>
</div>
`;
}
/** All todo.* entities, plus the saved value even when its integration
* hasn't loaded (so an offline pick isn't silently dropped from the UI). */
private _todoEntities(current: string): string[] {
const ids = Object.keys(this.hass?.states || {}).filter((id) => id.startsWith("todo.")).sort();
if (current && !ids.includes(current)) ids.unshift(current);
return ids;
}
// --- Section: Notifications ---
private _renderNotifications(L: string) {
@@ -762,7 +790,7 @@ export class MaintenanceSettingsView extends LitElement {
<label class="setting-row">
<span class="setting-label">${t("settings_notify_scope", L)}</span>
<select
.value=${n.scope_view_id || ""}
.value=${live(n.scope_view_id || "")}
@change=${(e: Event) => this._updateSetting("notify_scope_view_id", (e.target as HTMLSelectElement).value)}
>
<option value="" ?selected=${!n.scope_view_id}>${t("settings_notify_scope_all", L)}</option>
@@ -244,6 +244,7 @@ export class MaintenanceTaskDialog extends LitElement {
// NFC
@state() private _lastPerformed = "";
@state() private _nfcTagId = "";
@state() private _requireTagScan = false;
// 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,
@@ -263,12 +264,16 @@ export class MaintenanceTaskDialog extends LitElement {
// Task phases (#139): editable defs + the cycle order. The v1 editor keeps
// each phase to name + checklist + at most ONE own-object part (the mower
// "14 new blades" case); richer per-phase config exists at the WS level.
// `carry` holds def fields the editor has no form for (notes,
// required_completion_fields, future keys) and `extraParts` any links
// beyond the first — both re-merged on save so a no-op edit never wipes
// them (#106 pattern; same passthrough idea as trigger_config's carry).
// `carry` holds def fields the editor has no form for (notes, future
// keys) and `extraParts` any links beyond the first — both re-merged on
// save so a no-op edit never wipes them (#106 pattern; same passthrough
// idea as trigger_config's carry). Required completion fields are a real
// form now (#139 follow-up): `reqOverride` distinguishes "inherit the
// task's list" (off) from an explicit per-phase list — including the
// meaningful EMPTY override "this phase demands nothing".
@state() private _phaseDefs: Array<{
id: string; name: string; checklistText: string; partId: string; partQty: string;
reqOverride: boolean; reqFields: string[];
extraParts: TaskPartLink[]; carry: Record<string, unknown>;
}> = [];
@state() private _phaseSeq: string[] = [];
@@ -348,6 +353,11 @@ export class MaintenanceTaskDialog extends LitElement {
this._entryId = entryId;
this._taskId = task.id;
this._error = "";
// The object picker belongs to CREATE only. openCreate("", objects) left
// the choices behind, so Create → Cancel → Edit showed the dropdown in
// edit mode — and picking another object re-pointed _entryId while
// _taskId kept the old task (audit 2026-08-29).
this._objectChoices = [];
this._name = task.name;
this._type = task.type;
this._scheduleType = task.schedule_type;
@@ -389,6 +399,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._enabled = task.enabled !== false;
this._lastPerformed = task.last_performed || "";
this._nfcTagId = task.nfc_tag_id || "";
this._requireTagScan = !!task.require_tag_scan;
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).
@@ -401,14 +412,28 @@ export class MaintenanceTaskDialog extends LitElement {
this._checklistText = (task.checklist || []).join("\n");
this._phaseDefs = Object.entries(task.phases || {}).map(([id, def]) => {
const { name: _n, checklist: _c, consumes_parts: _p, ...carry } = def as unknown as Record<string, unknown>;
const {
name: _n, checklist: _c, consumes_parts: _p, required_completion_fields: _r,
...carry
} = def as unknown as Record<string, unknown>;
// The editor's single part field is the first OWN link (no entry_id —
// the picker only lists the object's own parts). Every other link,
// shared pools (#111) first among them, rides along whole in
// `extraParts` and is re-emitted on save: hydrating index 0 blindly
// turned a phase whose first link is foreign into an own-part link
// (or lost it) on the next save (audit 2026-08-29).
const links = def.consumes_parts || [];
const ownIdx = links.findIndex((l) => !l.entry_id);
const own = ownIdx >= 0 ? links[ownIdx] : undefined;
return {
id,
name: def.name || id,
checklistText: (def.checklist || []).join("\n"),
partId: def.consumes_parts?.[0]?.part_id || "",
partQty: def.consumes_parts?.[0]?.quantity != null ? String(def.consumes_parts[0].quantity) : "",
extraParts: (def.consumes_parts || []).slice(1),
partId: own?.part_id || "",
partQty: own?.quantity != null ? String(own.quantity) : "",
reqOverride: def.required_completion_fields !== undefined,
reqFields: [...(def.required_completion_fields || [])],
extraParts: links.filter((_, i) => i !== ownIdx).map((l) => ({ ...l })),
carry,
};
});
@@ -525,6 +550,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._enabled = true;
this._lastPerformed = "";
this._nfcTagId = "";
this._requireTagScan = false;
this._readingUnit = "";
this._consumesParts = {};
this._responsibleUserId = null;
@@ -1032,7 +1058,7 @@ export class MaintenanceTaskDialog extends LitElement {
private _addPhaseDef(): void {
const id = this._phaseSlug(`phase-${this._phaseDefs.length + 1}`);
this._phaseDefs = [...this._phaseDefs, { id, name: "", checklistText: "", partId: "", partQty: "", extraParts: [], carry: {} }];
this._phaseDefs = [...this._phaseDefs, { id, name: "", checklistText: "", partId: "", partQty: "", reqOverride: false, reqFields: [], extraParts: [], carry: {} }];
}
private _removePhaseDef(id: string): void {
@@ -1040,7 +1066,10 @@ export class MaintenanceTaskDialog extends LitElement {
this._phaseSeq = this._phaseSeq.filter((s) => s !== id);
}
private _patchPhaseDef(id: string, patch: Partial<{ name: string; checklistText: string; partId: string; partQty: string }>): void {
private _patchPhaseDef(
id: string,
patch: Partial<{ name: string; checklistText: string; partId: string; partQty: string; reqOverride: boolean; reqFields: string[] }>,
): void {
this._phaseDefs = this._phaseDefs.map((d) => (d.id === id ? { ...d, ...patch } : d));
}
@@ -1084,6 +1113,33 @@ export class MaintenanceTaskDialog extends LitElement {
@input=${(e: Event) => this._patchPhaseDef(d.id, { checklistText: (e.target as HTMLTextAreaElement).value })}
></textarea>
` : nothing}
<label class="req-option phase-req-toggle">
<input
type="checkbox"
.checked=${d.reqOverride}
@change=${(e: Event) => this._patchPhaseDef(d.id, { reqOverride: (e.target as HTMLInputElement).checked })}
/>
<span>${t("phase_require_override", L)}</span>
</label>
${d.reqOverride ? html`
<div class="required-completion phase-req-fields">
${REQUIRED_COMPLETION_KEYS.map((field) => html`
<label class="req-option">
<input
type="checkbox"
.checked=${d.reqFields.includes(field)}
@change=${(e: Event) => {
const on = (e.target as HTMLInputElement).checked;
const next = new Set(d.reqFields);
if (on) next.add(field); else next.delete(field);
this._patchPhaseDef(d.id, { reqFields: [...next] });
}}
/>
<span>${t(REQUIRED_COMPLETION_LABELS[field], L)}</span>
</label>
`)}
</div>
` : nothing}
</div>
`)}
<ha-button appearance="plain" @click=${this._addPhaseDef}>
@@ -1196,6 +1252,7 @@ export class MaintenanceTaskDialog extends LitElement {
data.enabled = this._enabled;
data.last_performed = this._lastPerformed || null;
data.nfc_tag_id = this._nfcTagId || null;
data.require_tag_scan = this._requireTagScan;
data.reading_unit = this._readingUnit.trim() || null;
// Task phases (#139): always sent — null clears a removed cycle.
{
@@ -1210,8 +1267,19 @@ export class MaintenanceTaskDialog extends LitElement {
const qty = parseFloat(d.partQty);
links.push({ part_id: d.partId, quantity: Number.isFinite(qty) && qty > 0 ? qty : 1 });
}
links.push(...d.extraParts);
// entry_id travels only when present (a shared pool, #111) — an
// own-part link stays byte-identical to what shipped before.
for (const l of d.extraParts) {
links.push(
l.entry_id
? { part_id: l.part_id, quantity: l.quantity, entry_id: l.entry_id }
: { part_id: l.part_id, quantity: l.quantity },
);
}
if (links.length) def.consumes_parts = links;
// Override on → send the list, EMPTY included ("demands nothing");
// off → omit the key so the task-level list falls through.
if (d.reqOverride) def.required_completion_fields = [...d.reqFields];
defs[d.id] = def;
}
const seq = this._phaseSeq.filter((id) => id in defs);
@@ -2432,7 +2500,7 @@ export class MaintenanceTaskDialog extends LitElement {
<div class="dialog-title">${title}</div>
<div class="content">
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
${this._objectChoices.length > 0 ? html`
${this._taskId === null && this._objectChoices.length > 0 ? html`
<div class="select-row">
<label>${t("object", L)}</label>
<select
@@ -2703,6 +2771,9 @@ export class MaintenanceTaskDialog extends LitElement {
${this._renderAdaptiveSection(L)}
<ms-textfield
label="${t("notes_optional", L)}"
multiline
.rows=${3}
.helper=${t("notes_markdown_hint", L)}
.value=${this._notes}
@input=${(e: Event) => (this._notes = (e.target as HTMLInputElement).value)}
></ms-textfield>
@@ -2751,6 +2822,15 @@ export class MaintenanceTaskDialog extends LitElement {
</div>
`
}
<label class="req-option">
<input
type="checkbox"
.checked=${this._requireTagScan}
@change=${(e: Event) => (this._requireTagScan = (e.target as HTMLInputElement).checked)}
/>
<span>${t("require_tag_scan", L)}</span>
</label>
${this._requireTagScan ? html`<div class="field-help">${t("require_tag_scan_help", L)}</div>` : nothing}
<label class="toggle-row">
<input
type="checkbox"
@@ -2923,6 +3003,15 @@ export class MaintenanceTaskDialog extends LitElement {
min-height: 56px;
margin-top: 6px;
}
.phase-req-toggle {
margin-top: 6px;
font-size: 13px;
color: var(--secondary-text-color);
}
.phase-req-fields {
margin: 2px 0 0 22px;
font-size: 13px;
}
.phase-remove {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
@@ -17,8 +17,8 @@ import { property, state } from "lit/decorators.js";
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence, langOf } from "../styles";
import { describeWsError } from "../ws-errors";
import { isoDateLocal } from "../helpers/calendar-bucket";
import { partsForCompletion } from "../helpers/shared-parts";
import { effectivePhase, phaseLabel } from "../helpers/phases";
import { buildCompleteDialogArgs } from "../helpers/complete-dialog-args";
import { phaseLabel } from "../helpers/phases";
import { renderWeibullSection } from "../renderers/weibull";
import { renderPredictionSection } from "../renderers/prediction";
import { renderRecommendationBars } from "../renderers/recommendation";
@@ -63,6 +63,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
};
@state() private _toast = "";
private _featuresLoaded = false;
/** Currency symbol for the complete dialog's cost suggestion — read off
* the same settings response as the feature flags. */
private _currencySymbol = "";
private get _lang(): string {
return langOf(this.hass);
@@ -93,10 +96,12 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
try {
const r = await this.hass.connection.sendMessagePromise<{
features?: Partial<AdvancedFeatures>;
budget?: { currency_symbol?: string };
}>({ type: "maintenance_supporter/settings" });
if (r?.features) {
this._features = { ...this._features, ...r.features };
}
this._currencySymbol = r?.budget?.currency_symbol || "";
this._featuresLoaded = true;
} catch {
// Settings endpoint unavailable — leave defaults (all false), the
@@ -158,35 +163,34 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
// (bug audit 2026-08-22).
import("../dialog-mount").then(async ({ openCompleteDialog }) => {
const task = this._task!;
const isBuy = !!(task as { part_ref?: string }).part_ref;
// #139: a phased task completes the phase currently due, so the phase's
// checklist/parts/required fields replace the task-level ones here.
const phase = effectivePhase(task);
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
let parts: Parameters<typeof openCompleteDialog>[0]["parts"] = [];
if (!isBuy) {
try {
const r = await this.hass.connection.sendMessagePromise<{
objects: MaintenanceObjectResponse[];
}>({ type: "maintenance_supporter/objects", compact: true });
parts = partsForCompletion({ consumes_parts: links }, this._entryId!, r.objects || [], this._lang);
} catch {
// Parts stay empty — the dialog still completes without them.
}
// The object list resolves the parts picker (own inventory + shared
// pools, #99/#111), the "consumes" hint lines and a buy task's restock
// default — fetched for every task kind, since a buy task needs its
// part's restock quantity + unit cost too.
let objects: MaintenanceObjectResponse[] = [];
try {
const r = await this.hass.connection.sendMessagePromise<{
objects: MaintenanceObjectResponse[];
}>({ type: "maintenance_supporter/objects", compact: true });
objects = r.objects || [];
} catch {
// Parts stay empty — the dialog still completes without them.
}
const ok = openCompleteDialog({
entry_id: this._entryId!,
task_id: this._taskId!,
task_name: task.name,
checklist: phase ? phase.checklist : (task.checklist || []),
adaptive_enabled: !!task.adaptive_config?.enabled,
required_completion_fields: phase ? phase.requiredFields : (task.required_completion_fields || []),
task_type: task.type || "",
reading_unit: (task as { reading_unit?: string }).reading_unit || "",
parts,
consumes_parts: isBuy ? [] : links,
phase_label: phaseLabel(task),
});
// Same derivation as the panel and the card (phase override, tag-scan
// gate, checklist ticks …) — see helpers/complete-dialog-args.
const ok = openCompleteDialog(
buildCompleteDialogArgs({
entryId: this._entryId!,
taskId: this._taskId!,
taskName: task.name,
task,
objects,
lang: this._lang,
checklist: task.checklist || [],
adaptiveEnabled: !!task.adaptive_config?.enabled,
currencySymbol: this._currencySymbol,
}),
);
if (ok) {
this._notifyChanged("complete");
this.close();
@@ -45,6 +45,7 @@ import type { MaintenanceTaskQuickActionsDialog } from "./components/task-quick-
import type { MaintenanceObjectQuickActionsDialog } from "./components/object-quick-actions-dialog";
import type { HomeAssistant, MaintenanceObject } from "./types";
import { ensureLocale, isLocaleLoaded, langOf } from "./styles";
import { fillAndOpenCompleteDialog, type CompleteDialogArgs } from "./helpers/complete-dialog-args";
const OBJECT_DIALOG_TAG = "maintenance-object-dialog";
const TASK_DIALOG_TAG = "maintenance-task-dialog";
@@ -274,41 +275,16 @@ export function openHistoryEditDialog(draft: HistoryEntryDraft): boolean {
return true;
}
/** v2.3.0: open the rich complete-dialog from any Lovelace context. */
export function openCompleteDialog(args: {
entry_id: string;
task_id: string;
task_name: string;
checklist?: string[];
adaptive_enabled?: boolean;
/** Details the task demands before it counts as done (v2.44). */
required_completion_fields?: string[];
/** Reading tasks need type+unit or the value field never renders. */
task_type?: string;
reading_unit?: string;
/** #99/#111: per-completion parts selection incl. shared pools. */
parts?: MaintenanceCompleteDialog["parts"];
consumes_parts?: MaintenanceCompleteDialog["consumesParts"];
/** #139: "2/4 · Flip blades" — the phase this completion records. */
phase_label?: string;
}): boolean {
/** v2.3.0: open the rich complete-dialog from any Lovelace context.
*
* Takes the full argument bag (`buildCompleteDialogArgs` derives it from a
* task + object list). Every dialog field is assigned — the dialog is a
* singleton, so an omitted field must reset, never leak the previous
* task's value (tag-scan gate, restock default, checklist ticks included). */
export function openCompleteDialog(args: CompleteDialogArgs): boolean {
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
if (!syncHass(dlg)) return false;
dlg.entryId = args.entry_id;
dlg.taskId = args.task_id;
dlg.taskName = args.task_name;
dlg.checklist = args.checklist ?? [];
dlg.adaptiveEnabled = !!args.adaptive_enabled;
dlg.requiredFields = args.required_completion_fields ?? [];
// Always assign — the dialog is a singleton, so an omitted field must not
// leak the previous task's value.
dlg.taskType = args.task_type ?? "";
dlg.readingUnit = args.reading_unit ?? "";
dlg.parts = args.parts ?? [];
dlg.consumesParts = args.consumes_parts ?? [];
dlg.phaseLabel = args.phase_label ?? "";
dlg.lang = (getHass()?.language) || "en";
dlg.open();
fillAndOpenCompleteDialog(dlg, args, (getHass()?.language) || "en");
return true;
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.",
"at_time": "v",
"notes_optional": "Poznámky (volitelné)",
"notes_markdown_hint": "Markdown je podporován — **tučně**, seznamy, [odkazy](…)",
"cost_optional": "Náklady (volitelné)",
"duration_minutes": "Doba trvání v minutách (volitelné)",
"completed_at_optional": "Dokončeno dne (volitelné, prázdné = nyní)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Název panelu",
"settings_notifications": "Oznámení",
"settings_notify_service": "Služba oznámení",
"settings_shopping_list": "Nákupní seznam (nákupní úkoly)",
"settings_shopping_list_help": "Upozornění na nákup docházejících dílů se objeví v tomto seznamu; odškrtnutí doplní zásobu.",
"shopping_list_none": "Vypnuto — žádný nákupní seznam",
"settings_install_assist_sentences": "Nainstalovat věty pro Assist",
"settings_install_assist_sentences_hint": "Zkopíruje hlasové věty do vaší konfigurace, aby je klasický agent Assist rozpoznal. Soubor, který jste upravili, nebude nikdy přepsán.",
"test_notification": "Testovací oznámení",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Přidat krok",
"phase_current": "Aktuální fáze",
"phase_set": "Nastavit jako aktuální",
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)"
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)",
"chart_history_alarm": "Pohled spouštěče z historie stavů: 1 = stav výstrahy držen po dobu přidržení, 0 = v pořádku (obvykle posledních ~10 dní)",
"chart_history_count": "Počet změn sestaven z historie stavů od posledního servisu (obvykle posledních ~10 dní)",
"prediction_cycles": "Naučeno z cyklů",
"phase_require_override": "Přepsat „Vyžadovat při dokončení“ pro tuto fázi",
"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'.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Tom = midnat (standard). HA-tidszone.",
"at_time": "kl.",
"notes_optional": "Noter (valgfrit)",
"notes_markdown_hint": "Markdown understøttes — **fed**, lister, [links](…)",
"cost_optional": "Omkostning (valgfrit)",
"duration_minutes": "Varighed i minutter (valgfrit)",
"completed_at_optional": "Udført den (valgfrit, tomt = nu)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Sidepanelets titel",
"settings_notifications": "Notifikationer",
"settings_notify_service": "Notifikationstjeneste",
"settings_shopping_list": "Indkøbsliste (købsopgaver)",
"settings_shopping_list_help": "Købspåmindelser for dele med lav beholdning vises på denne liste; afkrydsning genopfylder delen.",
"shopping_list_none": "Fra — ingen indkøbsliste",
"settings_install_assist_sentences": "Installer Assist-sætninger",
"settings_install_assist_sentences_hint": "Kopierer stemmesætningerne til din konfiguration, så den klassiske Assist-agent genkender dem. En fil, du selv har redigeret, overskrives aldrig.",
"test_notification": "Testnotifikation",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Tilføj trin",
"phase_current": "Aktuel fase",
"phase_set": "Sæt som aktuel",
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)"
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)",
"chart_history_alarm": "Udløservisning fra tilstandshistorikken: 1 = alarmtilstand fastholdt i holdetiden, 0 = ok (typisk de seneste ~10 dage)",
"chart_history_count": "Antal skift genopbygget fra tilstandshistorikken siden sidste service (typisk de seneste ~10 dage)",
"prediction_cycles": "Lært af cyklusser",
"phase_require_override": "Tilsidesæt „Kræv ved udførelse“ for denne fase",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.",
"at_time": "um",
"notes_optional": "Notizen (optional)",
"notes_markdown_hint": "Markdown wird unterstützt — **fett**, Listen, [Links](…)",
"cost_optional": "Kosten (optional)",
"duration_minutes": "Dauer in Minuten (optional)",
"completed_at_optional": "Erledigt am (optional, leer = jetzt)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Panel-Titel",
"settings_notifications": "Benachrichtigungen",
"settings_notify_service": "Benachrichtigungsdienst",
"settings_shopping_list": "Einkaufsliste (Kauf-Aufgaben)",
"settings_shopping_list_help": "Kauf-Erinnerungen für knappe Teile erscheinen in dieser To-do-Liste; Abhaken füllt den Bestand auf.",
"shopping_list_none": "Aus — keine Einkaufsliste",
"settings_install_assist_sentences": "Assist-Sätze installieren",
"settings_install_assist_sentences_hint": "Kopiert die Sprachbefehle in deine Konfiguration, damit der klassische Assist-Agent sie erkennt. Eine selbst bearbeitete Datei wird nie überschrieben.",
"test_notification": "Test-Benachrichtigung",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Schritt anhängen",
"phase_current": "Aktuelle Phase",
"phase_set": "Als aktuell setzen",
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)"
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)",
"chart_history_alarm": "Trigger-Sicht aus dem Recorder-Zustandsverlauf: 1 = Alarmzustand über die Haltezeit gehalten, 0 = in Ordnung (typisch die letzten ~10 Tage)",
"chart_history_count": "Wechselzähler aus dem Recorder-Zustandsverlauf seit dem letzten Service (typisch die letzten ~10 Tage)",
"prediction_cycles": "Gelernt aus Zyklen",
"phase_require_override": "„Bei Erledigung verlangen“ für diese Phase überschreiben",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Empty = midnight (default). HA timezone.",
"at_time": "at",
"notes_optional": "Notes (optional)",
"notes_markdown_hint": "Markdown is supported — **bold**, lists, [links](…)",
"cost_optional": "Cost (optional)",
"duration_minutes": "Duration in minutes (optional)",
"completed_at_optional": "Completed at (optional, empty = now)",
@@ -453,6 +454,9 @@
"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 — 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",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Add step",
"phase_current": "Current phase",
"phase_set": "Set as current",
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)"
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)",
"chart_history_alarm": "Trigger view from recorder state history: 1 = alert state held for the hold time, 0 = fine (typically the last ~10 days)",
"chart_history_count": "Change count rebuilt from recorder state history since the last service (typically the last ~10 days)",
"prediction_cycles": "Learned from cycles",
"phase_require_override": "Override “Require on completion” for this phase",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.",
"at_time": "a las",
"notes_optional": "Notas (opcional)",
"notes_markdown_hint": "Se admite Markdown — **negrita**, listas, [enlaces](…)",
"cost_optional": "Coste (opcional)",
"duration_minutes": "Duración en minutos (opcional)",
"completed_at_optional": "Completado el (opcional, vacío = ahora)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Título del panel lateral",
"settings_notifications": "Notificaciones",
"settings_notify_service": "Servicio de notificación",
"settings_shopping_list": "Lista de la compra (tareas de compra)",
"settings_shopping_list_help": "Los recordatorios de compra de piezas escasas aparecen en esta lista; marcar uno repone la pieza.",
"shopping_list_none": "Desactivado — sin lista de la compra",
"settings_install_assist_sentences": "Instalar frases de Assist",
"settings_install_assist_sentences_hint": "Copia las frases de voz en tu configuración para que el agente clásico de Assist las reconozca. Un archivo que hayas editado nunca se sobrescribe.",
"test_notification": "Notificación de prueba",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Añadir paso",
"phase_current": "Fase actual",
"phase_set": "Establecer como actual",
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)"
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)",
"chart_history_alarm": "Vista del disparador desde el historial de estados: 1 = estado de alerta mantenido durante el tiempo de retención, 0 = correcto (normalmente los últimos ~10 días)",
"chart_history_count": "Recuento de cambios reconstruido desde el historial de estados desde el último servicio (normalmente los últimos ~10 días)",
"prediction_cycles": "Aprendido de ciclos",
"phase_require_override": "Sobrescribir «Requerir al completar» para esta fase",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.",
"at_time": "klo",
"notes_optional": "Muistiinpanot (valinnainen)",
"notes_markdown_hint": "Markdown on tuettu — **lihavointi**, listat, [linkit](…)",
"cost_optional": "Kustannus (valinnainen)",
"duration_minutes": "Kesto minuutteina (valinnainen)",
"completed_at_optional": "Suoritettu (valinnainen, tyhjä = nyt)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Sivupalkin paneelin otsikko",
"settings_notifications": "Ilmoitukset",
"settings_notify_service": "Ilmoituspalvelu",
"settings_shopping_list": "Ostoslista (ostotehtävät)",
"settings_shopping_list_help": "Vähissä olevien osien ostomuistutukset näkyvät tässä luettelossa; kuittaus täydentää varaston.",
"shopping_list_none": "Pois — ei ostoslistaa",
"settings_install_assist_sentences": "Asenna Assist-lauseet",
"settings_install_assist_sentences_hint": "Kopioi äänikomennot asetuksiisi, jotta perinteinen Assist-agentti tunnistaa ne. Itse muokkaamaasi tiedostoa ei koskaan korvata.",
"test_notification": "Testi-ilmoitus",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Lisää askel",
"phase_current": "Nykyinen vaihe",
"phase_set": "Aseta nykyiseksi",
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)"
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)",
"chart_history_alarm": "Laukaisimen näkymä tilahistoriasta: 1 = hälytystila pysyi pitoajan, 0 = kunnossa (yleensä viimeiset ~10 päivää)",
"chart_history_count": "Muutoslaskuri koottu tilahistoriasta viime huollon jälkeen (yleensä viimeiset ~10 päivää)",
"prediction_cycles": "Opittu sykleistä",
"phase_require_override": "Ohita ”Vaadi valmistuessa” tälle vaiheelle",
"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.",
"require_tag_scan_hint": "Tämä tehtävä valmistuu vain skannaamalla sen NFC-tunniste tai QR-koodi itse laitteesta — tallennus tässä estetään."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.",
"at_time": "à",
"notes_optional": "Notes (optionnel)",
"notes_markdown_hint": "Markdown est pris en charge — **gras**, listes, [liens](…)",
"cost_optional": "Coût (optionnel)",
"duration_minutes": "Durée en minutes (optionnel)",
"completed_at_optional": "Effectué le (optionnel, vide = maintenant)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Titre du panneau latéral",
"settings_notifications": "Notifications",
"settings_notify_service": "Service de notification",
"settings_shopping_list": "Liste de courses (tâches d'achat)",
"settings_shopping_list_help": "Les rappels d'achat pour pièces en rupture apparaissent dans cette liste ; cocher un élément réapprovisionne la pièce.",
"shopping_list_none": "Désactivé — pas de liste de courses",
"settings_install_assist_sentences": "Installer les phrases Assist",
"settings_install_assist_sentences_hint": "Copie les phrases vocales dans votre configuration afin que l'agent Assist classique les reconnaisse. Un fichier que vous avez modifié n'est jamais écrasé.",
"test_notification": "Notification de test",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Ajouter une étape",
"phase_current": "Phase actuelle",
"phase_set": "Définir comme actuelle",
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)"
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)",
"chart_history_alarm": "Vue déclencheur issue de l'historique d'états : 1 = état d'alerte maintenu pendant le délai, 0 = normal (généralement les ~10 derniers jours)",
"chart_history_count": "Nombre de changements reconstitué à partir de l'historique d'états depuis le dernier entretien (généralement les ~10 derniers jours)",
"prediction_cycles": "Appris sur cycles",
"phase_require_override": "Remplacer « Exiger à l’achèvement » pour cette phase",
"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.",
"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é."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।",
"at_time": "बजे",
"notes_optional": "टिप्पणियाँ (वैकल्पिक)",
"notes_markdown_hint": "Markdown समर्थित है — **बोल्ड**, सूचियाँ, [लिंक](…)",
"cost_optional": "लागत (वैकल्पिक)",
"duration_minutes": "मिनटों में अवधि (वैकल्पिक)",
"completed_at_optional": "पूर्ण होने का समय (वैकल्पिक, खाली = अभी)",
@@ -453,6 +454,9 @@
"settings_panel_title": "साइडबार पैनल शीर्षक",
"settings_notifications": "सूचनाएँ",
"settings_notify_service": "सूचना सेवा",
"settings_shopping_list": "शॉपिंग सूची (खरीद कार्य)",
"settings_shopping_list_help": "कम स्टॉक वाले पुर्जों के खरीद अनुस्मारक इस सूची में दिखते हैं; टिक करने पर पुर्जा री-स्टॉक होता है।",
"shopping_list_none": "बंद — कोई शॉपिंग सूची नहीं",
"settings_install_assist_sentences": "Assist वाक्य इंस्टॉल करें",
"settings_install_assist_sentences_hint": "वॉइस वाक्यों को आपके कॉन्फ़िगरेशन में कॉपी करता है ताकि क्लासिक Assist एजेंट उन्हें पहचान सके। आपके द्वारा संपादित फ़ाइल कभी अधिलेखित नहीं होती।",
"test_notification": "परीक्षण सूचना",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "कदम जोड़ें",
"phase_current": "वर्तमान चरण",
"phase_set": "वर्तमान बनाएँ",
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)"
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)",
"chart_history_alarm": "स्थिति इतिहास से ट्रिगर दृश्य: 1 = चेतावनी स्थिति होल्ड समय तक बनी रही, 0 = सामान्य (आमतौर पर पिछले ~10 दिन)",
"chart_history_count": "पिछली सेवा के बाद से स्थिति इतिहास से पुनर्निर्मित परिवर्तन गणना (आमतौर पर पिछले ~10 दिन)",
"prediction_cycles": "चक्रों से सीखा",
"phase_require_override": "इस चरण के लिए “पूर्ण करते समय आवश्यक” को ओवरराइड करें",
"history_add_past": "पिछली पूर्णता जोड़ें",
"require_tag_scan": "केवल टैग स्कैन से पूर्ण करें",
"require_tag_scan_help": "उपस्थिति प्रमाण: जब तक वस्तु पर लगा NFC टैग या QR कोड स्कैन न हो, हर जगह “पूर्ण” अवरुद्ध रहता है। ऑटोमेशन complete सेवा को 'via_tag_scan' दे सकते हैं।",
"require_tag_scan_hint": "यह कार्य केवल वस्तु पर लगे NFC टैग या QR कोड को स्कैन करके ही पूर्ण होता है — यहाँ सहेजना अस्वीकार होगा।"
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Üres = éjfél (alapértelmezés). HA időzóna.",
"at_time": "ekkor:",
"notes_optional": "Megjegyzések (opcionális)",
"notes_markdown_hint": "A Markdown támogatott — **félkövér**, listák, [hivatkozások](…)",
"cost_optional": "Költség (opcionális)",
"duration_minutes": "Időtartam percben (opcionális)",
"completed_at_optional": "Elvégezve ekkor (opcionális, üres = most)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Oldalsáv-panel címe",
"settings_notifications": "Értesítések",
"settings_notify_service": "Értesítési szolgáltatás",
"settings_shopping_list": "Bevásárlólista (vásárlási feladatok)",
"settings_shopping_list_help": "A fogyóban lévő alkatrészek vásárlási emlékeztetői ezen a listán jelennek meg; a kipipálás feltölti a készletet.",
"shopping_list_none": "Ki — nincs bevásárlólista",
"settings_install_assist_sentences": "Assist mondatok telepítése",
"settings_install_assist_sentences_hint": "Átmásolja a hangmondatokat a konfigurációdba, hogy a klasszikus Assist ügynök felismerje őket. Az általad szerkesztett fájlt soha nem írja felül.",
"test_notification": "Tesztértesítés",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Lépés hozzáadása",
"phase_current": "Aktuális fázis",
"phase_set": "Beállítás aktuálisként",
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)"
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)",
"chart_history_alarm": "Trigger nézet az állapotelőzményekből: 1 = riasztási állapot a tartási idő alatt fennállt, 0 = rendben (általában az utolsó ~10 nap)",
"chart_history_count": "Változásszámláló az állapotelőzményekből az utolsó szerviz óta (általában az utolsó ~10 nap)",
"prediction_cycles": "Ciklusokból tanulva",
"phase_require_override": "A „Megkövetelés elvégzéskor” felülbírálása ennél a fázisnál",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.",
"at_time": "alle",
"notes_optional": "Note (opzionale)",
"notes_markdown_hint": "Markdown è supportato — **grassetto**, elenchi, [link](…)",
"cost_optional": "Costo (opzionale)",
"duration_minutes": "Durata in minuti (opzionale)",
"completed_at_optional": "Completato il (opzionale, vuoto = adesso)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Titolo pannello laterale",
"settings_notifications": "Notifiche",
"settings_notify_service": "Servizio di notifica",
"settings_shopping_list": "Lista della spesa (attività di acquisto)",
"settings_shopping_list_help": "I promemoria di acquisto per ricambi scarsi appaiono in questo elenco; spuntarne uno rifornisce il ricambio.",
"shopping_list_none": "Disattivato — nessuna lista della spesa",
"settings_install_assist_sentences": "Installa le frasi di Assist",
"settings_install_assist_sentences_hint": "Copia le frasi vocali nella tua configurazione affinché l'agente Assist classico le riconosca. Un file modificato da te non viene mai sovrascritto.",
"test_notification": "Notifica di test",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Aggiungi passo",
"phase_current": "Fase attuale",
"phase_set": "Imposta come attuale",
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)"
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)",
"chart_history_alarm": "Vista del trigger dallo storico stati: 1 = stato di allarme mantenuto per il tempo di attesa, 0 = ok (in genere gli ultimi ~10 giorni)",
"chart_history_count": "Conteggio dei cambi ricostruito dallo storico stati dall'ultimo intervento (in genere gli ultimi ~10 giorni)",
"prediction_cycles": "Appreso da cicli",
"phase_require_override": "Sostituisci «Richiedi al completamento» per questa fase",
"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.",
"require_tag_scan_hint": "Questa attività si completa solo scansionando il suo tag NFC o codice QR sull'oggetto — il salvataggio qui verrà rifiutato."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。",
"at_time": "時刻",
"notes_optional": "メモ(任意)",
"notes_markdown_hint": "Markdownに対応 — **太字**、リスト、[リンク](…)",
"cost_optional": "費用(任意)",
"duration_minutes": "所要時間(分、任意)",
"completed_at_optional": "完了日時(任意・空欄 = 現在)",
@@ -453,6 +454,9 @@
"settings_panel_title": "サイドバーパネルのタイトル",
"settings_notifications": "通知",
"settings_notify_service": "通知サービス",
"settings_shopping_list": "買い物リスト(購入タスク)",
"settings_shopping_list_help": "在庫が少ない部品の購入リマインダーがこのリストに表示され、チェックすると部品が補充されます。",
"shopping_list_none": "オフ — 買い物リストなし",
"settings_install_assist_sentences": "Assist の文を導入する",
"settings_install_assist_sentences_hint": "音声フレーズを設定に複製し、従来の Assist エージェントが認識できるようにします。自分で編集したファイルが上書きされることはありません。",
"test_notification": "テスト通知",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "ステップを追加",
"phase_current": "現在のフェーズ",
"phase_set": "現在に設定",
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)"
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)",
"chart_history_alarm": "状態履歴からのトリガー表示:1 = 保持時間の間アラート状態が続いた、0 = 正常(通常は直近約10日)",
"chart_history_count": "前回のサービス以降の状態履歴から再構築した変化回数(通常は直近約10日)",
"prediction_cycles": "学習したサイクル数",
"phase_require_override": "このフェーズで「完了時に必須」を上書き",
"history_add_past": "過去の完了を追加",
"require_tag_scan": "タグのスキャンでのみ完了可能",
"require_tag_scan_help": "在席証明:対象物のNFCタグまたはQRコードをスキャンするまで、どの画面でも「完了」はブロックされます。オートメーションはcompleteサービスに 'via_tag_scan' を渡せます。",
"require_tag_scan_hint": "このタスクは対象物のNFCタグまたはQRコードをスキャンしないと完了できません。ここでの保存は拒否されます。"
}
@@ -128,6 +128,7 @@
"schedule_time_help": "비워두면 자정(기본값)입니다. HA 시간대 기준.",
"at_time": "시각",
"notes_optional": "메모 (선택)",
"notes_markdown_hint": "Markdown 지원 — **굵게**, 목록, [링크](…)",
"cost_optional": "비용 (선택)",
"duration_minutes": "소요 시간(분, 선택)",
"completed_at_optional": "완료 시각 (선택, 비우면 지금)",
@@ -453,6 +454,9 @@
"settings_panel_title": "사이드바 패널 제목",
"settings_notifications": "알림",
"settings_notify_service": "알림 서비스",
"settings_shopping_list": "쇼핑 목록 (구매 작업)",
"settings_shopping_list_help": "부족한 부품의 구매 알림이 이 목록에 표시되며, 체크하면 부품이 재입고됩니다.",
"shopping_list_none": "끄기 — 쇼핑 목록 없음",
"settings_install_assist_sentences": "Assist 문장 설치",
"settings_install_assist_sentences_hint": "음성 문장을 설정에 복사하여 기존 Assist 에이전트가 인식하도록 합니다. 직접 수정한 파일은 절대 덮어쓰지 않습니다.",
"test_notification": "테스트 알림",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "스텝 추가",
"phase_current": "현재 단계",
"phase_set": "현재로 설정",
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)"
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)",
"chart_history_alarm": "상태 기록 기반 트리거 보기: 1 = 유지 시간 동안 경보 상태 유지, 0 = 정상 (보통 최근 약 10일)",
"chart_history_count": "마지막 서비스 이후 상태 기록으로 재구성한 변경 횟수 (보통 최근 약 10일)",
"prediction_cycles": "학습한 주기 수",
"phase_require_override": "이 단계에서 ‘완료 시 필수’ 재정의",
"history_add_past": "지난 완료 추가",
"require_tag_scan": "태그 스캔으로만 완료 가능",
"require_tag_scan_help": "현장 확인: 기기의 NFC 태그나 QR 코드를 스캔할 때까지 모든 화면에서 '완료'가 차단됩니다. 자동화는 complete 서비스에 'via_tag_scan'을 전달할 수 있습니다.",
"require_tag_scan_hint": "이 작업은 기기의 NFC 태그나 QR 코드를 스캔해야만 완료됩니다. 여기서 저장하면 거부됩니다."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Tom = midnatt (standard). HA-tidssone.",
"at_time": "kl.",
"notes_optional": "Notater (valgfritt)",
"notes_markdown_hint": "Markdown støttes — **fet**, lister, [lenker](…)",
"cost_optional": "Kostnad (valgfritt)",
"duration_minutes": "Varighet i minutter (valgfritt)",
"completed_at_optional": "Utført den (valgfritt, tomt = nå)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Tittel på sidepanel",
"settings_notifications": "Varsler",
"settings_notify_service": "Varslingstjeneste",
"settings_shopping_list": "Handleliste (kjøpsoppgaver)",
"settings_shopping_list_help": "Kjøpspåminnelser for deler med lav beholdning vises i denne listen; avkryssing fyller på beholdningen.",
"shopping_list_none": "Av — ingen handleliste",
"settings_install_assist_sentences": "Installer Assist-setninger",
"settings_install_assist_sentences_hint": "Kopierer talesetningene til konfigurasjonen din slik at den klassiske Assist-agenten gjenkjenner dem. En fil du selv har endret, blir aldri overskrevet.",
"test_notification": "Testvarsel",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Legg til trinn",
"phase_current": "Gjeldende fase",
"phase_set": "Sett som gjeldende",
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)"
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)",
"chart_history_alarm": "Utløservisning fra tilstandshistorikken: 1 = alarmtilstand holdt gjennom holdetiden, 0 = ok (vanligvis de siste ~10 dagene)",
"chart_history_count": "Antall endringer gjenoppbygd fra tilstandshistorikken siden siste service (vanligvis de siste ~10 dagene)",
"prediction_cycles": "Lært av sykluser",
"phase_require_override": "Overstyr «Krev ved fullføring» for denne fasen",
"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.",
"require_tag_scan_hint": "Denne oppgaven fullføres bare ved å skanne NFC-taggen eller QR-koden på selve tingen — lagring her vil bli avvist."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.",
"at_time": "om",
"notes_optional": "Notities (optioneel)",
"notes_markdown_hint": "Markdown wordt ondersteund — **vet**, lijsten, [links](…)",
"cost_optional": "Kosten (optioneel)",
"duration_minutes": "Duur in minuten (optioneel)",
"completed_at_optional": "Voltooid op (optioneel, leeg = nu)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Titel zijbalkpaneel",
"settings_notifications": "Meldingen",
"settings_notify_service": "Meldingsservice",
"settings_shopping_list": "Boodschappenlijst (kooptaken)",
"settings_shopping_list_help": "Koopherinneringen voor schaarse onderdelen verschijnen in deze lijst; afvinken vult de voorraad aan.",
"shopping_list_none": "Uit — geen boodschappenlijst",
"settings_install_assist_sentences": "Assist-zinnen installeren",
"settings_install_assist_sentences_hint": "Kopieert de spraakzinnen naar je configuratie zodat de klassieke Assist-agent ze herkent. Een bestand dat je zelf hebt bewerkt wordt nooit overschreven.",
"test_notification": "Testmelding",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Stap toevoegen",
"phase_current": "Huidige fase",
"phase_set": "Instellen als huidige",
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)"
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)",
"chart_history_alarm": "Triggerweergave uit de statusgeschiedenis: 1 = alarmstatus gedurende de wachttijd aangehouden, 0 = in orde (meestal de laatste ~10 dagen)",
"chart_history_count": "Aantal wisselingen opnieuw opgebouwd uit de statusgeschiedenis sinds de laatste service (meestal de laatste ~10 dagen)",
"prediction_cycles": "Geleerd uit cycli",
"phase_require_override": "„Vereisen bij afronden” voor deze fase overschrijven",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.",
"at_time": "o",
"notes_optional": "Notatki (opcjonalne)",
"notes_markdown_hint": "Obsługiwany jest Markdown — **pogrubienie**, listy, [linki](…)",
"cost_optional": "Koszt (opcjonalne)",
"duration_minutes": "Czas trwania w minutach (opcjonalne)",
"completed_at_optional": "Wykonano dnia (opcjonalne, puste = teraz)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Tytuł panelu bocznego",
"settings_notifications": "Powiadomienia",
"settings_notify_service": "Usługa powiadomień",
"settings_shopping_list": "Lista zakupów (zadania zakupowe)",
"settings_shopping_list_help": "Przypomnienia o zakupie brakujących części pojawiają się na tej liście; odhaczenie uzupełnia zapas.",
"shopping_list_none": "Wyłączone — brak listy zakupów",
"settings_install_assist_sentences": "Zainstaluj zdania Assist",
"settings_install_assist_sentences_hint": "Kopiuje zdania głosowe do Twojej konfiguracji, aby klasyczny agent Assist je rozpoznawał. Plik zmieniony przez Ciebie nigdy nie zostanie nadpisany.",
"test_notification": "Powiadomienie testowe",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Dodaj krok",
"phase_current": "Bieżąca faza",
"phase_set": "Ustaw jako bieżącą",
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)"
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)",
"chart_history_alarm": "Widok wyzwalacza z historii stanów: 1 = stan alarmu utrzymany przez czas podtrzymania, 0 = w porządku (zwykle ostatnie ~10 dni)",
"chart_history_count": "Licznik zmian odtworzony z historii stanów od ostatniego serwisu (zwykle ostatnie ~10 dni)",
"prediction_cycles": "Nauczone z cykli",
"phase_require_override": "Nadpisz „Wymagaj przy ukończeniu” dla tej fazy",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário do HA.",
"at_time": "às",
"notes_optional": "Observações (opcional)",
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [links](…)",
"cost_optional": "Custo (opcional)",
"duration_minutes": "Duração em minutos (opcional)",
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Título do painel na barra lateral",
"settings_notifications": "Notificações",
"settings_notify_service": "Serviço de notificação",
"settings_shopping_list": "Lista de compras (tarefas de compra)",
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
"shopping_list_none": "Desligado — sem lista de compras",
"settings_install_assist_sentences": "Instalar frases do Assist",
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um arquivo que você editou nunca é sobrescrito.",
"test_notification": "Notificação de teste",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Adicionar passo",
"phase_current": "Fase atual",
"phase_set": "Definir como atual",
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)"
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)",
"chart_history_alarm": "Visão do gatilho a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
"prediction_cycles": "Aprendido de ciclos",
"phase_require_override": "Substituir “Exigir na conclusão” para esta fase",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.",
"at_time": "às",
"notes_optional": "Notas (opcional)",
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [ligações](…)",
"cost_optional": "Custo (opcional)",
"duration_minutes": "Duração em minutos (opcional)",
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Título do painel lateral",
"settings_notifications": "Notificações",
"settings_notify_service": "Serviço de notificação",
"settings_shopping_list": "Lista de compras (tarefas de compra)",
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
"shopping_list_none": "Desligado — sem lista de compras",
"settings_install_assist_sentences": "Instalar frases do Assist",
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um ficheiro que editou nunca é substituído.",
"test_notification": "Notificação de teste",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Adicionar passo",
"phase_current": "Fase atual",
"phase_set": "Definir como atual",
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)"
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)",
"chart_history_alarm": "Vista do disparador a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
"prediction_cycles": "Aprendido de ciclos",
"phase_require_override": "Substituir «Exigir na conclusão» para esta fase",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.",
"at_time": "в",
"notes_optional": "Примечания (опционально)",
"notes_markdown_hint": "Поддерживается Markdown — **жирный**, списки, [ссылки](…)",
"cost_optional": "Стоимость (опционально)",
"duration_minutes": "Длительность в минутах (опционально)",
"completed_at_optional": "Выполнено (необязательно, пусто = сейчас)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Заголовок панели",
"settings_notifications": "Уведомления",
"settings_notify_service": "Сервис уведомлений",
"settings_shopping_list": "Список покупок (задачи покупки)",
"settings_shopping_list_help": "Напоминания о покупке заканчивающихся деталей появляются в этом списке; отметка пополняет запас.",
"shopping_list_none": "Выкл. — без списка покупок",
"settings_install_assist_sentences": "Установить фразы Assist",
"settings_install_assist_sentences_hint": "Копирует голосовые фразы в вашу конфигурацию, чтобы классический агент Assist их распознавал. Файл, который вы изменили, никогда не перезаписывается.",
"test_notification": "Тестовое уведомление",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Добавить шаг",
"phase_current": "Текущая фаза",
"phase_set": "Сделать текущей",
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)"
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)",
"chart_history_alarm": "Вид триггера из истории состояний: 1 = состояние тревоги удерживалось в течение времени удержания, 0 = норма (обычно последние ~10 дней)",
"chart_history_count": "Счётчик изменений восстановлен из истории состояний с момента последнего обслуживания (обычно последние ~10 дней)",
"prediction_cycles": "Изучено по циклам",
"phase_require_override": "Переопределить «Требовать при выполнении» для этой фазы",
"history_add_past": "Добавить прошлое выполнение",
"require_tag_scan": "Завершение только по скану метки",
"require_tag_scan_help": "Подтверждение присутствия: «Готово» заблокировано везде, пока не отсканирована NFC-метка или QR-код на самом устройстве. Автоматизации могут передавать 'via_tag_scan' в службу complete.",
"require_tag_scan_hint": "Эта задача завершается только сканированием её NFC-метки или QR-кода на самом устройстве — сохранение здесь будет отклонено."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.",
"at_time": "kl.",
"notes_optional": "Anteckningar (valfritt)",
"notes_markdown_hint": "Markdown stöds — **fetstil**, listor, [länkar](…)",
"cost_optional": "Kostnad (valfritt)",
"duration_minutes": "Varaktighet i minuter (valfritt)",
"completed_at_optional": "Utförd den (valfritt, tomt = nu)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Sidopanelens titel",
"settings_notifications": "Notifikationer",
"settings_notify_service": "Notifikationstjänst",
"settings_shopping_list": "Inköpslista (köpuppgifter)",
"settings_shopping_list_help": "Köppåminnelser för delar som tar slut visas i denna lista; att bocka av en fyller på lagret.",
"shopping_list_none": "Av — ingen inköpslista",
"settings_install_assist_sentences": "Installera Assist-meningar",
"settings_install_assist_sentences_hint": "Kopierar rösmeningarna till din konfiguration så att den klassiska Assist-agenten känner igen dem. En fil du själv har ändrat skrivs aldrig över.",
"test_notification": "Testnotifikation",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Lägg till steg",
"phase_current": "Aktuell fas",
"phase_set": "Ange som aktuell",
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)"
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)",
"chart_history_alarm": "Utlösarvy från tillståndshistoriken: 1 = larmläge hållet under hålltiden, 0 = ok (vanligtvis de senaste ~10 dagarna)",
"chart_history_count": "Antal ändringar återskapat från tillståndshistoriken sedan senaste service (vanligtvis de senaste ~10 dagarna)",
"prediction_cycles": "Lärt av cykler",
"phase_require_override": "Åsidosätt ”Kräv vid slutförande” för denna fas",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Boş = gece yarısı (varsayılan). HA saat dilimi.",
"at_time": "saat",
"notes_optional": "Notlar (isteğe bağlı)",
"notes_markdown_hint": "Markdown desteklenir — **kalın**, listeler, [bağlantılar](…)",
"cost_optional": "Maliyet (isteğe bağlı)",
"duration_minutes": "Dakika cinsinden süre (isteğe bağlı)",
"completed_at_optional": "Tamamlanma zamanı (isteğe bağlı, boş = şimdi)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Kenar çubuğu paneli başlığı",
"settings_notifications": "Bildirimler",
"settings_notify_service": "Bildirim servisi",
"settings_shopping_list": "Alışveriş listesi (satın alma görevleri)",
"settings_shopping_list_help": "Azalan parçalar için satın alma hatırlatıcıları bu listede görünür; işaretlemek parçayı yeniden stoklar.",
"shopping_list_none": "Kapalı — alışveriş listesi yok",
"settings_install_assist_sentences": "Assist cümlelerini yükle",
"settings_install_assist_sentences_hint": "Sesli komut cümlelerini yapılandırmanıza kopyalar, böylece klasik Assist aracısı bunları tanır. Kendi düzenlediğiniz bir dosyanın üzerine asla yazılmaz.",
"test_notification": "Test bildirimi",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Adım ekle",
"phase_current": "Geçerli aşama",
"phase_set": "Geçerli olarak ayarla",
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)"
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)",
"chart_history_alarm": "Durum geçmişinden tetikleyici görünümü: 1 = uyarı durumu bekletme süresi boyunca sürdü, 0 = normal (genellikle son ~10 gün)",
"chart_history_count": "Değişim sayısı son servisten bu yana durum geçmişinden yeniden oluşturuldu (genellikle son ~10 gün)",
"prediction_cycles": "Döngülerden öğrenildi",
"phase_require_override": "Bu aşama için “Tamamlarken zorunlu tut” ayarını geçersiz kıl",
"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.",
"require_tag_scan_hint": "Bu görev yalnızca cihazın üzerindeki NFC etiketi veya QR kodu taranarak tamamlanır — buradan kaydetme reddedilecektir."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.",
"at_time": "о",
"notes_optional": "Примітки (необов'язково)",
"notes_markdown_hint": "Підтримується Markdown — **жирний**, списки, [посилання](…)",
"cost_optional": "Вартість (необов'язково)",
"duration_minutes": "Тривалість у хвилинах (необов'язково)",
"completed_at_optional": "Виконано (необов'язково, порожньо = зараз)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Заголовок панелі",
"settings_notifications": "Сповіщення",
"settings_notify_service": "Служба сповіщень",
"settings_shopping_list": "Список покупок (завдання покупки)",
"settings_shopping_list_help": "Нагадування про купівлю деталей, що закінчуються, з'являються в цьому списку; відмітка поповнює запас.",
"shopping_list_none": "Вимк. — без списку покупок",
"settings_install_assist_sentences": "Встановити фрази Assist",
"settings_install_assist_sentences_hint": "Копіює голосові фрази до вашої конфігурації, щоб класичний агент Assist їх розпізнавав. Файл, який ви редагували, ніколи не перезаписується.",
"test_notification": "Тестове сповіщення",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Додати крок",
"phase_current": "Поточна фаза",
"phase_set": "Зробити поточною",
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)"
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)",
"chart_history_alarm": "Вигляд тригера з історії станів: 1 = стан тривоги утримувався протягом часу утримання, 0 = норма (зазвичай останні ~10 днів)",
"chart_history_count": "Лічильник змін відновлено з історії станів від останнього обслуговування (зазвичай останні ~10 днів)",
"prediction_cycles": "Вивчено з циклів",
"phase_require_override": "Перевизначити «Вимагати при виконанні» для цієї фази",
"history_add_past": "Додати минуле виконання",
"require_tag_scan": "Завершення лише скануванням мітки",
"require_tag_scan_help": "Підтвердження присутності: «Готово» заблоковано всюди, доки не відскановано NFC-мітку або QR-код на самому пристрої. Автоматизації можуть передати 'via_tag_scan' службі complete.",
"require_tag_scan_hint": "Це завдання завершується лише скануванням його NFC-мітки або QR-коду на самому пристрої — збереження тут буде відхилено."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。",
"at_time": "于",
"notes_optional": "备注 (可选)",
"notes_markdown_hint": "支持 Markdown — **加粗**、列表、[链接](…)",
"cost_optional": "成本 (可选)",
"duration_minutes": "耗时 (分钟, 可选)",
"completed_at_optional": "完成时间(可选,留空 = 现在)",
@@ -453,6 +454,9 @@
"settings_panel_title": "侧边栏面板标题",
"settings_notifications": "通知",
"settings_notify_service": "通知服务",
"settings_shopping_list": "购物清单(购买任务)",
"settings_shopping_list_help": "库存不足部件的购买提醒会显示在此清单中;勾选即可补充库存。",
"shopping_list_none": "关闭 — 无购物清单",
"settings_install_assist_sentences": "安装 Assist 语句",
"settings_install_assist_sentences_hint": "将语音语句复制到你的配置中,让传统 Assist 代理能够识别它们。你自己修改过的文件永远不会被覆盖。",
"test_notification": "测试通知",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "添加步骤",
"phase_current": "当前阶段",
"phase_set": "设为当前",
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)"
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)",
"chart_history_alarm": "来自状态历史的触发视图:1 = 警报状态保持了保持时间,0 = 正常(通常为最近约 10 天)",
"chart_history_count": "根据上次维护以来的状态历史重建的变化次数(通常为最近约 10 天)",
"prediction_cycles": "已学习的周期数",
"phase_require_override": "为此阶段覆盖“完成时要求”",
"history_add_past": "补录过去的完成记录",
"require_tag_scan": "仅可通过扫描标签完成",
"require_tag_scan_help": "在场证明:在扫描设备上的 NFC 标签或二维码之前,所有界面的“完成”均被阻止。自动化可向 complete 服务传递 'via_tag_scan'。",
"require_tag_scan_hint": "此任务只能通过扫描设备上的 NFC 标签或二维码来完成——在此保存将被拒绝。"
}
@@ -18,10 +18,11 @@ import type {
SavedViewFilters,
} from "./types";
import { UserService } from "./user-service";
import { partsForCompletion } from "./helpers/shared-parts";
import { effectivePhase, phaseLabel } from "./helpers/phases";
import { phaseLabel } from "./helpers/phases";
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
import "./maintenance-card-editor";
import "./components/complete-dialog";
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
// The Battery Fleet card ships in this globally-loaded bundle so it is
// available on every dashboard without another extra_module_url entry.
import "./components/battery-fleet-card";
@@ -514,32 +515,26 @@ export class MaintenanceSupporterCard extends LitElement {
@click=${(e: Event) => {
// Stop the row's open-task handler from also firing
e.stopPropagation();
const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog") as any;
// #139: a phased task completes the phase
// currently due — its fields override the
// task-level ones (fallthrough included).
const phase = effectivePhase(task);
dlg.entryId = entry_id;
dlg.taskId = task.id;
dlg.taskName = task.name;
dlg.checklist = phase ? phase.checklist : (task.checklist || []);
dlg.adaptiveEnabled = !!task.adaptive_config?.enabled;
dlg.taskType = task.type || "";
dlg.readingUnit = (task as any).reading_unit || "";
dlg.requiredFields = phase ? phase.requiredFields : (task.required_completion_fields || []);
dlg.phaseLabel = phaseLabel(task);
dlg.lang = L;
// #99: editable per-completion parts selection
// (skip on buy tasks — those restock instead).
// #111: the list also carries the shared pools
// this task draws on, each named after its
// owner — resolving against the object's own
// parts alone left a foreign link invisible.
const isBuy = !!(task as any).part_ref;
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
dlg.parts = isBuy ? [] : partsForCompletion({ consumes_parts: links }, entry_id, this._objects, L);
dlg.consumesParts = isBuy ? [] : links;
dlg.open();
const dlg = this.shadowRoot!.querySelector<MaintenanceCompleteDialog>("maintenance-complete-dialog")!;
// The SAME derivation the panel uses (phase
// override, parts incl. shared pools, tag-scan
// gate, restock default, checklist ticks) — the
// card used to forward a subset.
fillAndOpenCompleteDialog(
dlg,
buildCompleteDialogArgs({
entryId: entry_id,
taskId: task.id,
taskName: task.name,
task,
objects: this._objects,
lang: L,
checklist: task.checklist || [],
adaptiveEnabled: !!task.adaptive_config?.enabled,
currencySymbol: this._stats?.budget?.currency_symbol || "",
}),
L,
);
}}
>
<ha-icon icon="mdi:check"></ha-icon>
@@ -15,9 +15,11 @@ import { buildObjectReportHtml, type ReportLabels } from "./helpers/report";
import { warrantyStatus } from "./helpers/warranty";
import { OBJECT_COLUMNS, DEFAULT_OBJECTS_TABLE_COLUMNS, sanitizeColumns } from "./helpers/object-columns";
import { downloadTextFile } from "./helpers/download";
import { renderNotesMarkdown } from "./helpers/notes-markdown";
import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet";
import { describePartLink, partsForCompletion } from "./helpers/shared-parts";
import { effectivePhase, phaseLabel } from "./helpers/phases";
import { describePartLink } from "./helpers/shared-parts";
import { effectivePhase } from "./helpers/phases";
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
import { describeWsError } from "./ws-errors";
import { panelStyles } from "./panel-styles";
import type {
@@ -694,6 +696,20 @@ export class MaintenanceSupporterPanel extends LitElement {
if ((msg as SubscriptionEvent<MaintenanceObjectResponse>).objects) {
writeObjectsCache(next, this._stats ?? null);
}
// The KPI row (_stats) and the budget card come from their own
// endpoints — a pushed change (completion from the card, a timer
// wave flipping a task overdue) left them frozen at the last
// _loadData until the next user action.
void this._refreshKpis();
// The open task's full history is a separate fetch too; the
// truncated list payload never wins the "longer list" comparison
// in _historyCtx, so a pushed completion stayed invisible there.
if (
this._view === "task" && this._selectedEntryId && this._selectedTaskId
&& (!!ev.objects || (ev.delta || []).some((d) => d.entry_id === this._selectedEntryId))
) {
void this._fetchFullHistory(this._selectedEntryId, this._selectedTaskId);
}
}
},
// deltas: only entries whose rebuilt response actually changed —
@@ -713,6 +729,34 @@ export class MaintenanceSupporterPanel extends LitElement {
}
}
/** Refetch the KPI statistics + budget status after a pushed object change.
* Coalesced: a burst of deltas (import, timer wave) runs at most one
* in-flight fetch plus one trailing catch-up instead of one per event. */
private _kpiRefreshInFlight = false;
private _kpiRefreshPending = false;
private async _refreshKpis(): Promise<void> {
if (this._kpiRefreshInFlight) {
this._kpiRefreshPending = true;
return;
}
this._kpiRefreshInFlight = true;
try {
do {
this._kpiRefreshPending = false;
const [statsResult, budgetResult] = await Promise.all([
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }).catch(() => null),
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/budget_status" }).catch(() => null),
]);
if (!this.isConnected) return;
if (statsResult) this._stats = statsResult as StatisticsResponse;
if (budgetResult) this._budget = budgetResult as BudgetStatus;
} while (this._kpiRefreshPending);
} finally {
this._kpiRefreshInFlight = false;
}
}
// --- Data accessors ---
private get _taskRows(): TaskRow[] {
@@ -1734,17 +1778,27 @@ export class MaintenanceSupporterPanel extends LitElement {
this._showToast(t("quick_complete_success", this._lang));
} catch (e: unknown) {
const code = (e as { code?: string })?.code || "";
if (code === "no_defaults") {
// No defaults set — open the dialog so the user can fill them in.
// Both refusals mean "the dialog has to collect something": no
// quick-complete defaults configured, or the task demands completion
// details a silent complete cannot supply. The dialog opens as the
// scan's fallback — via_tag_scan keeps the proof of presence.
if (code === "no_defaults" || code === "completion_details_required") {
this._openCompleteDialog(
entryId, taskId, task.name,
this._features.checklists ? task.checklist : undefined,
this._features.adaptive && !!task.adaptive_config?.enabled,
{ viaTagScan: true },
);
} else {
this._showToast(t("action_error", this._lang));
// Relay the server's reason (too_early carries the earliest date)
// instead of a generic "action failed".
this._showToast(describeWsError(e, this._lang, t("action_error", this._lang)));
}
return;
}
// Silent success: the list must reflect the completion right away — the
// subscription delta may lag or be coalesced.
try { await this._loadData(); } catch { /* subscription will sync */ }
}
// v2.21: printable one-pager for a task — details, checklist tick boxes,
@@ -1874,59 +1928,41 @@ export class MaintenanceSupporterPanel extends LitElement {
});
}
private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
private _openCompleteDialog(
entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean,
opts?: { viaTagScan?: boolean },
): void {
void this._ui<MaintenanceCompleteDialog>("maintenance-complete-dialog")
.then((dlg) => dlg && this._fillAndOpenCompleteDialog(dlg, entryId, taskId, taskName, checklist, adaptiveEnabled));
.then((dlg) => dlg && this._fillAndOpenCompleteDialog(dlg, entryId, taskId, taskName, checklist, adaptiveEnabled, opts));
}
private _fillAndOpenCompleteDialog(dlg: MaintenanceCompleteDialog, entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
dlg.entryId = entryId;
dlg.taskId = taskId;
dlg.taskName = taskName;
dlg.lang = this._lang;
dlg.checklist = checklist || [];
dlg.adaptiveEnabled = !!adaptiveEnabled;
// v2.20 (#83): reading tasks get a value field — resolve type + unit here
// so none of the many call sites need to thread them through.
const tk = this._objects
.find((o) => o.entry_id === entryId)
?.tasks.find((tsk) => tsk.id === taskId);
// #139: a phased task completes the phase currently due — its checklist,
// parts and required fields override the task-level values (fallthrough
// is already baked into effectivePhase). Resolved HERE so every call
// site of _openCompleteDialog rotates for free.
const phase = tk ? effectivePhase(tk) : null;
dlg.phaseLabel = phase ? phaseLabel(tk) : "";
dlg.taskType = tk?.type || "";
dlg.readingUnit = tk?.reading_unit || "";
// #73: ticks recorded during the cycle prefill the dialog's checklist.
dlg.checklistPrefill = tk?.checklist_progress || {};
dlg.requiredFields = phase ? phase.requiredFields : (tk?.required_completion_fields || []);
if (phase) {
dlg.checklist = this._features.checklists ? phase.checklist : [];
}
// Spare parts: a buy task gets an editable restock-qty field; a consuming
// task shows what it will decrement (incl. the storage location).
const objParts = this._objects.find((o) => o.entry_id === entryId)?.parts || [];
const refPart = tk?.part_ref ? objParts.find((pt) => pt.id === tk.part_ref!.part_id) : undefined;
dlg.restockDefault = tk?.part_ref ? (refPart?.restock_quantity ?? 1) : null;
// #104 follow-up: parts carry unit costs — the dialog offers their sum
// as a one-click cost suggestion (buy task: restock qty × unit cost).
dlg.restockUnitCost = tk?.part_ref ? (refPart?.cost ?? null) : null;
dlg.currencySymbol = this._currencySymbol;
// #111: a link may point at another object's pool — name that object, and
// never drop a line that fails to resolve (the old .filter(Boolean) hid it).
const links = phase ? phase.consumesParts : (tk?.consumes_parts || []);
dlg.consumesInfo = links.map((link) =>
describePartLink(link, entryId, this._objects, this._lang),
/** Every dialog field is derived in helpers/complete-dialog-args — the
* ONE derivation the panel, the Lovelace card and the quick-actions
* dialog share (phase override #139, reading type+unit #83, restock
* default + unit cost #104, shared pools #111, checklist ticks #73,
* tag-scan gate). Resolved HERE so none of the call sites need to
* thread anything but the name through. */
private _fillAndOpenCompleteDialog(
dlg: MaintenanceCompleteDialog, entryId: string, taskId: string, taskName: string,
checklist?: string[], adaptiveEnabled?: boolean, opts?: { viaTagScan?: boolean },
): void {
fillAndOpenCompleteDialog(
dlg,
buildCompleteDialogArgs({
entryId,
taskId,
taskName,
task: this._getTask(entryId, taskId),
objects: this._objects,
lang: this._lang,
checklist,
checklistsEnabled: this._features.checklists,
adaptiveEnabled,
currencySymbol: this._currencySymbol,
viaTagScan: opts?.viaTagScan,
}),
this._lang,
);
// #99: editable per-completion parts selection (not on buy tasks — those
// RESTOCK via the qty field instead of consuming). The list carries the
// object's own parts plus the shared pools this task draws on, so a foreign
// link is visible and untickable rather than silently absent.
dlg.parts = tk?.part_ref ? [] : partsForCompletion({ consumes_parts: links }, entryId, this._objects, this._lang);
dlg.consumesParts = tk?.part_ref ? [] : links;
dlg.open();
}
private _openQrForObject(entryId: string, objectName: string): void {
@@ -3287,7 +3323,7 @@ export class MaintenanceSupporterPanel extends LitElement {
${o.notes
? html`<div class="object-notes">
<div class="object-notes-label">${t("object_notes_label", L)}</div>
<div class="object-notes-body">${o.notes}</div>
<div class="object-notes-body">${renderNotesMarkdown(o.notes)}</div>
</div>`
: nothing}
@@ -445,6 +445,12 @@ export const panelStyles = css`
word-break: break-word;
line-height: 1.45;
}
/* Markdown notes: <ha-markdown> parses the source itself — the container's
pre-wrap must not leak into its rendered paragraphs. */
.object-notes-body ha-markdown,
.task-meta-notes ha-markdown {
white-space: normal;
}
.empty { color: var(--secondary-text-color); font-style: italic; }
.analysis-empty-state { text-align: center; padding: 24px 16px; }
.analysis-empty-state .empty { font-size: 15px; margin-bottom: 8px; }
@@ -1053,6 +1059,19 @@ export const panelStyles = css`
border-color: var(--primary-color);
}
/* #142: "add a past completion" entry point in the history tab — the
backdate field lives in the complete dialog, but people LOOK for it
here. */
.history-add-past {
display: flex;
justify-content: flex-end;
margin-bottom: 4px;
}
.history-add-past-btn ha-icon {
--mdc-icon-size: 18px;
margin-right: 4px;
}
/* #139: cycle-phase strip in the task overview */
.phases-card {
background: var(--card-background-color, #fff);
@@ -44,6 +44,7 @@ export function renderPredictionSection(task: MaintenanceTask, lang: string, fea
<span class="prediction-value prediction-days${task.days_until_threshold === 0 ? " exceeded" : task.sensor_prediction_urgency ? " urgent" : ""}">${task.days_until_threshold === 0 ? t("threshold_exceeded", lang) : "~" + Math.round(task.days_until_threshold!) + " " + t("days", lang)}</span>
${task.threshold_prediction_date ? html`<span class="prediction-date">${formatDate(task.threshold_prediction_date, lang)}</span>` : nothing}
${task.threshold_prediction_confidence ? html`<span class="confidence-dot ${task.threshold_prediction_confidence}"></span>` : nothing}
${(task.prediction_cycles ?? 0) > 0 ? html`<span class="prediction-cycles">${t("prediction_cycles", lang)}: ${task.prediction_cycles}</span>` : nothing}
</div>
` : nothing}
${hasEnv && features.environmental ? html`
@@ -39,6 +39,100 @@ export interface SparklineContext {
* [Q1 − 1.5·IQR, Q3 + 1.5·IQR]. Robust to a few wild glitch readings (a
* pressure sensor spiking to 100 while it normally sits at 1.5–3). No-ops on
* short series (< 4 points) where quartiles aren't meaningful. */
/** A binary HA state as the 0/1 level the history fallback plots, or null
* for states the fallback cannot express (then no trigger view is built). */
export function binaryLevel(state: string | null | undefined): 0 | 1 | null {
const s = (state ?? "").trim().toLowerCase();
if (s === "on" || s === "open" || s === "true") return 1;
if (s === "off" || s === "closed" || s === "false") return 0;
return null;
}
export interface StateChangeView {
points: ChartPoint[];
/** "alarm": single-shot latch (target 1 + a to-state) — 1 while the alert
* state is held, 0 otherwise. "count": change counter since the last
* service, stepped, honouring the hold filter. */
mode: "alarm" | "count";
}
/** #141 (round 2): the recorder fallback plots the RAW entity state, but a
* state_change trigger is about transitions. A water-valve switch that is
* "on" all week with a few second-long "off" blips drew a flat 1 with the
* "Target changes: 1" line on the same axis — while the header said 0 / 1,
* because those blips never held for the #136 hold time. Re-express the
* step series in the trigger's own terms:
*
* - latch (target_changes == 1 with a to-state): 1 = alert state HELD for
* the hold time, 0 = fine. Reads as "is the trigger active" — for a
* problem sensor (to-state on) this equals the raw line, for a switch
* watched for "off" it is the inverse the user expects.
* - counter (everything else): the cumulative number of matching, held
* transitions since the last service, anchored so the line ends at the
* trigger's live count (transitions before the recorder window are
* unknown but already in that count).
*
* Returns null when the from/to states cannot be expressed as 0/1. */
export function stateChangeView(
points: ChartPoint[],
tc: { trigger_from_state?: string | null; trigger_to_state?: string | null; trigger_target_changes?: number; trigger_for_minutes?: number },
opts: { since: number | null; current: number | null; now?: number },
): StateChangeView | null {
if (points.length < 2) return null;
const now = opts.now ?? Date.now();
const holdMs = Math.max(0, tc.trigger_for_minutes ?? 0) * 60000;
const fromLevel = tc.trigger_from_state ? binaryLevel(tc.trigger_from_state) : null;
const toLevel = tc.trigger_to_state ? binaryLevel(tc.trigger_to_state) : null;
if ((tc.trigger_from_state && fromLevel === null) || (tc.trigger_to_state && toLevel === null)) return null;
// Collapse the doubled step points into held segments (start, level).
const sorted = [...points].sort((a, b) => a.ts - b.ts);
const segments: { start: number; level: number }[] = [];
for (const p of sorted) {
const last = segments[segments.length - 1];
if (!last || last.level !== p.val) segments.push({ start: p.ts, level: p.val });
}
const held = (i: number) => {
const end = i + 1 < segments.length ? segments[i + 1].start : now;
return end - segments[i].start >= holdMs;
};
const step = (out: ChartPoint[], ts: number, val: number) => {
const last = out[out.length - 1];
if (last && last.val !== val) out.push({ ts, val: last.val });
out.push({ ts, val });
};
const latch = (tc.trigger_target_changes ?? 1) === 1 && toLevel !== null;
if (latch) {
const out: ChartPoint[] = [];
segments.forEach((seg, i) => step(out, seg.start, seg.level === toLevel && held(i) ? 1 : 0));
const lastVal = out[out.length - 1]?.val ?? 0;
out.push({ ts: now, val: lastVal });
return { points: out, mode: "alarm" };
}
const since = opts.since ?? segments[0].start;
let counted = 0;
const stepsAt: number[] = [];
for (let i = 1; i < segments.length; i++) {
const prev = segments[i - 1], seg = segments[i];
if (fromLevel !== null && prev.level !== fromLevel) continue;
if (toLevel !== null && seg.level !== toLevel) continue;
if (!held(i) || seg.start < since) continue;
counted += 1;
stepsAt.push(seg.start);
}
const base = Math.max(0, (opts.current ?? counted) - counted);
const out: ChartPoint[] = [{ ts: Math.max(since, segments[0].start), val: base }];
let val = base;
for (const ts of stepsAt) {
val += 1;
step(out, ts, val);
}
out.push({ ts: now, val });
return { points: out, mode: "count" };
}
export function filterOutliers(points: ChartPoint[]): ChartPoint[] {
if (points.length < 4) return points;
const vals = points.map((p) => p.val).sort((a, b) => a - b);
@@ -276,6 +370,13 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
const entityId = tc.entity_id || "";
let points = rawStatsPoints(task, ctx);
// #141 round 2: a state_change trigger on recorder history draws the
// trigger's view (alert held / change count), not the raw entity state.
let stateView: StateChangeView | null = null;
if (triggerType === "state_change" && entityId && ctx.historyFallbackIds?.has(entityId)) {
stateView = stateChangeView(points, tc, { since: lastServiceTs(task), current: task.trigger_current_value ?? null });
if (stateView) points = stateView.points;
}
// Runtime accumulates hours with no stored intermediate snapshots: synthesize
// the current cycle (0 at the last service → live value now) so the chart
// resets each completion and always has a drawable 2-point line.
@@ -326,8 +427,9 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
}
targetValue = tc.trigger_target_value;
forceZero = true;
} else if (triggerType === "state_change" && tc.trigger_target_changes) {
// Change counts / runtime hours already accumulate from zero.
} else if (triggerType === "state_change" && tc.trigger_target_changes && stateView?.mode !== "alarm") {
// Change counts / runtime hours already accumulate from zero. A latch
// ("alarm" view) has no meaningful target line: its axis is alert/ok.
targetValue = tc.trigger_target_changes;
forceZero = true;
} else if (triggerType === "runtime" && tc.trigger_runtime_hours) {
@@ -341,15 +443,25 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
// exists — a slow 0.25 %/day decline classifies stable yet still crosses
// the threshold in a foreseeable number of days.
let projection: ChartPoint[] | null = null;
// A projection pointing AWAY from the configured threshold is noise (the
// ET-4800 finding: a refill-skewed rising rate on a below-threshold ink
// sensor projected the ink level going UP) — draw it only when it heads
// toward the bound, or when no bound is configured at all.
const rate = task.degradation_rate;
const pointsAway =
rate != null &&
((tc.trigger_below != null && tc.trigger_above == null && rate > 0) ||
(tc.trigger_above != null && tc.trigger_below == null && rate < 0));
if (
targetValue == null &&
task.degradation_rate != null &&
rate != null &&
!pointsAway &&
(task.degradation_trend !== "stable" || task.days_until_threshold != null) &&
task.degradation_trend !== "insufficient_data" &&
points.length >= 2
) {
const lp = points[points.length - 1];
projection = [lp, { ts: lp.ts + 30 * 86400000, val: lp.val + task.degradation_rate * 30 }];
projection = [lp, { ts: lp.ts + 30 * 86400000, val: lp.val + rate * 30 }];
}
const events: ChartEvent[] = task.history
@@ -380,7 +492,7 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
if (entityId && ctx.historyFallbackIds?.has(entityId) && !statsFetchedEmpty) {
return html`<div class="chart-note">
<ha-icon icon="mdi:information-outline"></ha-icon>
${t("chart_history_fallback", ctx.lang)}
${t(stateView?.mode === "alarm" ? "chart_history_alarm" : stateView?.mode === "count" ? "chart_history_count" : "chart_history_fallback", ctx.lang)}
</div>`;
}
if (statsFetchedEmpty) {
@@ -11,6 +11,7 @@
import { html, nothing } from "lit";
import { isSafeHttpUrl } from "../helpers/url";
import { renderNotesMarkdown } from "../helpers/notes-markdown";
import { t, formatDate, formatDateTime, formatRecurrence } from "../styles";
import type { AdvancedFeatures, HomeAssistant, MaintenanceTask, ManualDocRef } from "../types";
import { renderTriggerSection, type SparklineContext } from "./sparkline";
@@ -222,8 +223,8 @@ function renderPhasesCard(task: MaintenanceTask, ctx: TaskDetailContext) {
const last = lastByPhase.get(id);
return html`
<div class="phase-step ${i === cursor ? "current" : ""}"
title=${i === cursor ? t("phase_current", L) : (ctx.isOperator ? t("phase_set", L) : "")}
@click=${() => { if (ctx.isOperator && i !== cursor) ctx.setPhaseCursor(i); }}>
title=${i === cursor ? t("phase_current", L) : t("phase_set", L)}
@click=${() => { if (i !== cursor) ctx.setPhaseCursor(i); }}>
<span class="phase-step-name">${i + 1}. ${name}</span>
${last ? html`<span class="phase-step-last">${formatDate(last, L)}</span>` : nothing}
</div>
@@ -284,7 +285,7 @@ function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) {
${task.notes ? html`
<div class="task-meta-row">
<ha-icon icon="mdi:note-text-outline"></ha-icon>
<span class="task-meta-notes">${task.notes}</span>
<span class="task-meta-notes">${renderNotesMarkdown(task.notes)}</span>
</div>
` : nothing}
${safeTaskUrl ? html`
@@ -494,6 +495,12 @@ export function renderOverviewTab(task: MaintenanceTask, ctx: TaskDetailContext)
function renderHistoryTab(task: MaintenanceTask, ctx: TaskDetailContext) {
return html`
<div class="tab-content history-tab">
<div class="history-add-past">
<ha-button appearance="plain" class="history-add-past-btn" @click=${() => ctx.openComplete(task)}>
<ha-icon icon="mdi:calendar-plus"></ha-icon>
${t("history_add_past", ctx.lang)}
</ha-button>
</div>
${renderHistoryFilters(task, ctx.history)}
${renderHistoryList(task, ctx.history)}
</div>
@@ -99,10 +99,15 @@ export function t(key: string, lang?: string): string {
* in English until then rather than as raw keys). Was copy-pasted across the
* panel and both cards (drift audit 2026-08). */
export function syncLocaleFromHass(
host: { hass?: { language?: string; locale?: unknown }; requestUpdate: () => void },
host: {
hass?: { language?: string; locale?: unknown; config?: { country?: string | null } };
requestUpdate: () => void;
},
changedProps: Map<string, unknown>,
): void {
if (changedProps.has("hass")) setDateTimePrefs(host.hass?.locale as Parameters<typeof setDateTimePrefs>[0]);
if (changedProps.has("hass")) {
setDateTimePrefs(host.hass?.locale as Parameters<typeof setDateTimePrefs>[0], host.hass?.config?.country);
}
const lang = host.hass?.language;
if (lang && !isLocaleLoaded(lang)) {
ensureLocale(lang).then(() => host.requestUpdate());
@@ -196,15 +201,41 @@ function langToLocale(lang?: string): string {
interface DateTimePrefs {
date?: string;
time?: string;
country?: string;
}
const _w = window as unknown as { __msDateTimePrefs?: DateTimePrefs };
const DT_PREFS: DateTimePrefs = (_w.__msDateTimePrefs ??= {});
/** Feed HA's per-user date/time format into formatDate/formatDateTime. */
export function setDateTimePrefs(locale?: { date_format?: string; time_format?: string }): void {
/** Feed HA's per-user date/time format into formatDate/formatDateTime.
* `country` is the SERVER's configured country (#140): with the profile
* format left on "language" it regionalizes the language default —
* "en" + AU renders DD/MM/YYYY instead of the hard en-US mapping. */
export function setDateTimePrefs(
locale?: { date_format?: string; time_format?: string },
country?: string | null,
): void {
if (!locale) return;
DT_PREFS.date = locale.date_format;
DT_PREFS.time = locale.time_format;
if (country !== undefined) DT_PREFS.country = country || undefined;
}
/** BCP-47 locale for the "language" default: the UI language regionalized
* by the server country when Intl knows the combination (#140). Explicit
* profile formats (DMY/MDY/YMD/system) never reach this path. */
function resolveLocale(lang?: string): string {
const base = langToLocale(lang);
const country = DT_PREFS.country;
if (country && /^[A-Za-z]{2}$/.test(country)) {
const candidate = `${normLang(lang).split("-")[0]}-${country.toUpperCase()}`;
try {
new Intl.DateTimeFormat(candidate);
return candidate;
} catch {
// syntactically invalid tag — keep the language mapping
}
}
return base;
}
function _formatDateObj(d: Date, lang?: string): string {
@@ -217,17 +248,17 @@ function _formatDateObj(d: Date, lang?: string): string {
case "YMD": return `${yyyy}-${mm}-${dd}`;
case "system":
return d.toLocaleDateString(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
default: // "language" or unset — derive from the UI language (pre-#97 behaviour)
return d.toLocaleDateString(langToLocale(lang), { day: "2-digit", month: "2-digit", year: "numeric" });
default: // "language" or unset — UI language, regionalized by the server country (#140)
return d.toLocaleDateString(resolveLocale(lang), { day: "2-digit", month: "2-digit", year: "numeric" });
}
}
function _formatTimeObj(d: Date, lang?: string): string {
switch (DT_PREFS.time) {
case "12": return d.toLocaleTimeString(langToLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: true });
case "24": return d.toLocaleTimeString(langToLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: false });
case "12": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: true });
case "24": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: false });
case "system": return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
default: return d.toLocaleTimeString(langToLocale(lang), { hour: "2-digit", minute: "2-digit" });
default: return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit" });
}
}
@@ -1291,6 +1322,11 @@ export const sharedStyles = css`
opacity: 0.7;
}
.prediction-cycles {
font-size: 11px;
opacity: 0.7;
}
.prediction-entity {
font-size: 10px;
opacity: 0.6;
@@ -248,6 +248,9 @@ export interface MaintenanceTask {
degradation_rate?: number | null;
degradation_trend?: string | null; // "rising" | "falling" | "stable" | "insufficient_data"
degradation_r_squared?: number | null;
/** How many prior service cycles the degradation rate learned from
* (sawtooth sensors); 0 = current window only. */
prediction_cycles?: number;
days_until_threshold?: number | null;
threshold_prediction_date?: string | null;
threshold_prediction_confidence?: string | null; // "low" | "medium" | "high"
@@ -259,6 +262,8 @@ export interface MaintenanceTask {
responsible_user_id?: string | null;
custom_icon?: string | null;
nfc_tag_id?: string | null;
/** Proof of presence: completion only via NFC/QR scan on the thing. */
require_tag_scan?: boolean;
/** Number of documents linked to this task (paperclip badge on the row). */
document_count?: number;
/** v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...). */
@@ -560,6 +565,9 @@ export interface HomeAssistant {
// "system" | "DMY" | "MDY" | "YMD" resp. "language" | "system" | "12" | "24")
// — issue #97: dates must follow them, not just the UI language.
locale?: { language: string; number_format?: string; date_format?: string; time_format?: string };
/** Server config — country (#140) feeds the "language"-default date
* format ("en" + AU → en-AU → DD/MM/YYYY). */
config?: { country?: string | null };
localize(key: string, ...args: unknown[]): string;
user?: { id: string; name: string; is_admin: boolean; is_owner: boolean };
/** Current access token — used for authenticated `fetch()` to our HTTP
@@ -126,6 +126,7 @@
"schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.",
"at_time": "v",
"notes_optional": "Poznámky (volitelné)",
"notes_markdown_hint": "Markdown je podporován — **tučně**, seznamy, [odkazy](…)",
"cost_optional": "Náklady (volitelné)",
"duration_minutes": "Doba trvání v minutách (volitelné)",
"completed_at_optional": "Dokončeno dne (volitelné, prázdné = nyní)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Název panelu",
"settings_notifications": "Oznámení",
"settings_notify_service": "Služba oznámení",
"settings_shopping_list": "Nákupní seznam (nákupní úkoly)",
"settings_shopping_list_help": "Upozornění na nákup docházejících dílů se objeví v tomto seznamu; odškrtnutí doplní zásobu.",
"shopping_list_none": "Vypnuto — žádný nákupní seznam",
"settings_install_assist_sentences": "Nainstalovat věty pro Assist",
"settings_install_assist_sentences_hint": "Zkopíruje hlasové věty do vaší konfigurace, aby je klasický agent Assist rozpoznal. Soubor, který jste upravili, nebude nikdy přepsán.",
"test_notification": "Testovací oznámení",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Přidat krok",
"phase_current": "Aktuální fáze",
"phase_set": "Nastavit jako aktuální",
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)"
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)",
"chart_history_alarm": "Pohled spouštěče z historie stavů: 1 = stav výstrahy držen po dobu přidržení, 0 = v pořádku (obvykle posledních ~10 dní)",
"chart_history_count": "Počet změn sestaven z historie stavů od posledního servisu (obvykle posledních ~10 dní)",
"prediction_cycles": "Naučeno z cyklů",
"phase_require_override": "Přepsat „Vyžadovat při dokončení“ pro tuto fázi",
"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'.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Tom = midnat (standard). HA-tidszone.",
"at_time": "kl.",
"notes_optional": "Noter (valgfrit)",
"notes_markdown_hint": "Markdown understøttes — **fed**, lister, [links](…)",
"cost_optional": "Omkostning (valgfrit)",
"duration_minutes": "Varighed i minutter (valgfrit)",
"completed_at_optional": "Udført den (valgfrit, tomt = nu)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Sidepanelets titel",
"settings_notifications": "Notifikationer",
"settings_notify_service": "Notifikationstjeneste",
"settings_shopping_list": "Indkøbsliste (købsopgaver)",
"settings_shopping_list_help": "Købspåmindelser for dele med lav beholdning vises på denne liste; afkrydsning genopfylder delen.",
"shopping_list_none": "Fra — ingen indkøbsliste",
"settings_install_assist_sentences": "Installer Assist-sætninger",
"settings_install_assist_sentences_hint": "Kopierer stemmesætningerne til din konfiguration, så den klassiske Assist-agent genkender dem. En fil, du selv har redigeret, overskrives aldrig.",
"test_notification": "Testnotifikation",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Tilføj trin",
"phase_current": "Aktuel fase",
"phase_set": "Sæt som aktuel",
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)"
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)",
"chart_history_alarm": "Udløservisning fra tilstandshistorikken: 1 = alarmtilstand fastholdt i holdetiden, 0 = ok (typisk de seneste ~10 dage)",
"chart_history_count": "Antal skift genopbygget fra tilstandshistorikken siden sidste service (typisk de seneste ~10 dage)",
"prediction_cycles": "Lært af cyklusser",
"phase_require_override": "Tilsidesæt „Kræv ved udførelse“ for denne fase",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.",
"at_time": "um",
"notes_optional": "Notizen (optional)",
"notes_markdown_hint": "Markdown wird unterstützt — **fett**, Listen, [Links](…)",
"cost_optional": "Kosten (optional)",
"duration_minutes": "Dauer in Minuten (optional)",
"completed_at_optional": "Erledigt am (optional, leer = jetzt)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Panel-Titel",
"settings_notifications": "Benachrichtigungen",
"settings_notify_service": "Benachrichtigungsdienst",
"settings_shopping_list": "Einkaufsliste (Kauf-Aufgaben)",
"settings_shopping_list_help": "Kauf-Erinnerungen für knappe Teile erscheinen in dieser To-do-Liste; Abhaken füllt den Bestand auf.",
"shopping_list_none": "Aus — keine Einkaufsliste",
"settings_install_assist_sentences": "Assist-Sätze installieren",
"settings_install_assist_sentences_hint": "Kopiert die Sprachbefehle in deine Konfiguration, damit der klassische Assist-Agent sie erkennt. Eine selbst bearbeitete Datei wird nie überschrieben.",
"test_notification": "Test-Benachrichtigung",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Schritt anhängen",
"phase_current": "Aktuelle Phase",
"phase_set": "Als aktuell setzen",
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)"
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)",
"chart_history_alarm": "Trigger-Sicht aus dem Recorder-Zustandsverlauf: 1 = Alarmzustand über die Haltezeit gehalten, 0 = in Ordnung (typisch die letzten ~10 Tage)",
"chart_history_count": "Wechselzähler aus dem Recorder-Zustandsverlauf seit dem letzten Service (typisch die letzten ~10 Tage)",
"prediction_cycles": "Gelernt aus Zyklen",
"phase_require_override": "„Bei Erledigung verlangen“ für diese Phase überschreiben",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Empty = midnight (default). HA timezone.",
"at_time": "at",
"notes_optional": "Notes (optional)",
"notes_markdown_hint": "Markdown is supported — **bold**, lists, [links](…)",
"cost_optional": "Cost (optional)",
"duration_minutes": "Duration in minutes (optional)",
"completed_at_optional": "Completed at (optional, empty = now)",
@@ -453,6 +454,9 @@
"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 — 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",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Add step",
"phase_current": "Current phase",
"phase_set": "Set as current",
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)"
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)",
"chart_history_alarm": "Trigger view from recorder state history: 1 = alert state held for the hold time, 0 = fine (typically the last ~10 days)",
"chart_history_count": "Change count rebuilt from recorder state history since the last service (typically the last ~10 days)",
"prediction_cycles": "Learned from cycles",
"phase_require_override": "Override “Require on completion” for this phase",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.",
"at_time": "a las",
"notes_optional": "Notas (opcional)",
"notes_markdown_hint": "Se admite Markdown — **negrita**, listas, [enlaces](…)",
"cost_optional": "Coste (opcional)",
"duration_minutes": "Duración en minutos (opcional)",
"completed_at_optional": "Completado el (opcional, vacío = ahora)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Título del panel lateral",
"settings_notifications": "Notificaciones",
"settings_notify_service": "Servicio de notificación",
"settings_shopping_list": "Lista de la compra (tareas de compra)",
"settings_shopping_list_help": "Los recordatorios de compra de piezas escasas aparecen en esta lista; marcar uno repone la pieza.",
"shopping_list_none": "Desactivado — sin lista de la compra",
"settings_install_assist_sentences": "Instalar frases de Assist",
"settings_install_assist_sentences_hint": "Copia las frases de voz en tu configuración para que el agente clásico de Assist las reconozca. Un archivo que hayas editado nunca se sobrescribe.",
"test_notification": "Notificación de prueba",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Añadir paso",
"phase_current": "Fase actual",
"phase_set": "Establecer como actual",
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)"
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)",
"chart_history_alarm": "Vista del disparador desde el historial de estados: 1 = estado de alerta mantenido durante el tiempo de retención, 0 = correcto (normalmente los últimos ~10 días)",
"chart_history_count": "Recuento de cambios reconstruido desde el historial de estados desde el último servicio (normalmente los últimos ~10 días)",
"prediction_cycles": "Aprendido de ciclos",
"phase_require_override": "Sobrescribir «Requerir al completar» para esta fase",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.",
"at_time": "klo",
"notes_optional": "Muistiinpanot (valinnainen)",
"notes_markdown_hint": "Markdown on tuettu — **lihavointi**, listat, [linkit](…)",
"cost_optional": "Kustannus (valinnainen)",
"duration_minutes": "Kesto minuutteina (valinnainen)",
"completed_at_optional": "Suoritettu (valinnainen, tyhjä = nyt)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Sivupalkin paneelin otsikko",
"settings_notifications": "Ilmoitukset",
"settings_notify_service": "Ilmoituspalvelu",
"settings_shopping_list": "Ostoslista (ostotehtävät)",
"settings_shopping_list_help": "Vähissä olevien osien ostomuistutukset näkyvät tässä luettelossa; kuittaus täydentää varaston.",
"shopping_list_none": "Pois — ei ostoslistaa",
"settings_install_assist_sentences": "Asenna Assist-lauseet",
"settings_install_assist_sentences_hint": "Kopioi äänikomennot asetuksiisi, jotta perinteinen Assist-agentti tunnistaa ne. Itse muokkaamaasi tiedostoa ei koskaan korvata.",
"test_notification": "Testi-ilmoitus",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Lisää askel",
"phase_current": "Nykyinen vaihe",
"phase_set": "Aseta nykyiseksi",
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)"
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)",
"chart_history_alarm": "Laukaisimen näkymä tilahistoriasta: 1 = hälytystila pysyi pitoajan, 0 = kunnossa (yleensä viimeiset ~10 päivää)",
"chart_history_count": "Muutoslaskuri koottu tilahistoriasta viime huollon jälkeen (yleensä viimeiset ~10 päivää)",
"prediction_cycles": "Opittu sykleistä",
"phase_require_override": "Ohita ”Vaadi valmistuessa” tälle vaiheelle",
"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.",
"require_tag_scan_hint": "Tämä tehtävä valmistuu vain skannaamalla sen NFC-tunniste tai QR-koodi itse laitteesta — tallennus tässä estetään."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.",
"at_time": "à",
"notes_optional": "Notes (optionnel)",
"notes_markdown_hint": "Markdown est pris en charge — **gras**, listes, [liens](…)",
"cost_optional": "Coût (optionnel)",
"duration_minutes": "Durée en minutes (optionnel)",
"completed_at_optional": "Effectué le (optionnel, vide = maintenant)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Titre du panneau latéral",
"settings_notifications": "Notifications",
"settings_notify_service": "Service de notification",
"settings_shopping_list": "Liste de courses (tâches d'achat)",
"settings_shopping_list_help": "Les rappels d'achat pour pièces en rupture apparaissent dans cette liste ; cocher un élément réapprovisionne la pièce.",
"shopping_list_none": "Désactivé — pas de liste de courses",
"settings_install_assist_sentences": "Installer les phrases Assist",
"settings_install_assist_sentences_hint": "Copie les phrases vocales dans votre configuration afin que l'agent Assist classique les reconnaisse. Un fichier que vous avez modifié n'est jamais écrasé.",
"test_notification": "Notification de test",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Ajouter une étape",
"phase_current": "Phase actuelle",
"phase_set": "Définir comme actuelle",
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)"
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)",
"chart_history_alarm": "Vue déclencheur issue de l'historique d'états : 1 = état d'alerte maintenu pendant le délai, 0 = normal (généralement les ~10 derniers jours)",
"chart_history_count": "Nombre de changements reconstitué à partir de l'historique d'états depuis le dernier entretien (généralement les ~10 derniers jours)",
"prediction_cycles": "Appris sur cycles",
"phase_require_override": "Remplacer « Exiger à l’achèvement » pour cette phase",
"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.",
"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é."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।",
"at_time": "बजे",
"notes_optional": "टिप्पणियाँ (वैकल्पिक)",
"notes_markdown_hint": "Markdown समर्थित है — **बोल्ड**, सूचियाँ, [लिंक](…)",
"cost_optional": "लागत (वैकल्पिक)",
"duration_minutes": "मिनटों में अवधि (वैकल्पिक)",
"completed_at_optional": "पूर्ण होने का समय (वैकल्पिक, खाली = अभी)",
@@ -453,6 +454,9 @@
"settings_panel_title": "साइडबार पैनल शीर्षक",
"settings_notifications": "सूचनाएँ",
"settings_notify_service": "सूचना सेवा",
"settings_shopping_list": "शॉपिंग सूची (खरीद कार्य)",
"settings_shopping_list_help": "कम स्टॉक वाले पुर्जों के खरीद अनुस्मारक इस सूची में दिखते हैं; टिक करने पर पुर्जा री-स्टॉक होता है।",
"shopping_list_none": "बंद — कोई शॉपिंग सूची नहीं",
"settings_install_assist_sentences": "Assist वाक्य इंस्टॉल करें",
"settings_install_assist_sentences_hint": "वॉइस वाक्यों को आपके कॉन्फ़िगरेशन में कॉपी करता है ताकि क्लासिक Assist एजेंट उन्हें पहचान सके। आपके द्वारा संपादित फ़ाइल कभी अधिलेखित नहीं होती।",
"test_notification": "परीक्षण सूचना",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "कदम जोड़ें",
"phase_current": "वर्तमान चरण",
"phase_set": "वर्तमान बनाएँ",
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)"
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)",
"chart_history_alarm": "स्थिति इतिहास से ट्रिगर दृश्य: 1 = चेतावनी स्थिति होल्ड समय तक बनी रही, 0 = सामान्य (आमतौर पर पिछले ~10 दिन)",
"chart_history_count": "पिछली सेवा के बाद से स्थिति इतिहास से पुनर्निर्मित परिवर्तन गणना (आमतौर पर पिछले ~10 दिन)",
"prediction_cycles": "चक्रों से सीखा",
"phase_require_override": "इस चरण के लिए “पूर्ण करते समय आवश्यक” को ओवरराइड करें",
"history_add_past": "पिछली पूर्णता जोड़ें",
"require_tag_scan": "केवल टैग स्कैन से पूर्ण करें",
"require_tag_scan_help": "उपस्थिति प्रमाण: जब तक वस्तु पर लगा NFC टैग या QR कोड स्कैन न हो, हर जगह “पूर्ण” अवरुद्ध रहता है। ऑटोमेशन complete सेवा को 'via_tag_scan' दे सकते हैं।",
"require_tag_scan_hint": "यह कार्य केवल वस्तु पर लगे NFC टैग या QR कोड को स्कैन करके ही पूर्ण होता है — यहाँ सहेजना अस्वीकार होगा।"
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Üres = éjfél (alapértelmezés). HA időzóna.",
"at_time": "ekkor:",
"notes_optional": "Megjegyzések (opcionális)",
"notes_markdown_hint": "A Markdown támogatott — **félkövér**, listák, [hivatkozások](…)",
"cost_optional": "Költség (opcionális)",
"duration_minutes": "Időtartam percben (opcionális)",
"completed_at_optional": "Elvégezve ekkor (opcionális, üres = most)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Oldalsáv-panel címe",
"settings_notifications": "Értesítések",
"settings_notify_service": "Értesítési szolgáltatás",
"settings_shopping_list": "Bevásárlólista (vásárlási feladatok)",
"settings_shopping_list_help": "A fogyóban lévő alkatrészek vásárlási emlékeztetői ezen a listán jelennek meg; a kipipálás feltölti a készletet.",
"shopping_list_none": "Ki — nincs bevásárlólista",
"settings_install_assist_sentences": "Assist mondatok telepítése",
"settings_install_assist_sentences_hint": "Átmásolja a hangmondatokat a konfigurációdba, hogy a klasszikus Assist ügynök felismerje őket. Az általad szerkesztett fájlt soha nem írja felül.",
"test_notification": "Tesztértesítés",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Lépés hozzáadása",
"phase_current": "Aktuális fázis",
"phase_set": "Beállítás aktuálisként",
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)"
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)",
"chart_history_alarm": "Trigger nézet az állapotelőzményekből: 1 = riasztási állapot a tartási idő alatt fennállt, 0 = rendben (általában az utolsó ~10 nap)",
"chart_history_count": "Változásszámláló az állapotelőzményekből az utolsó szerviz óta (általában az utolsó ~10 nap)",
"prediction_cycles": "Ciklusokból tanulva",
"phase_require_override": "A „Megkövetelés elvégzéskor” felülbírálása ennél a fázisnál",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.",
"at_time": "alle",
"notes_optional": "Note (opzionale)",
"notes_markdown_hint": "Markdown è supportato — **grassetto**, elenchi, [link](…)",
"cost_optional": "Costo (opzionale)",
"duration_minutes": "Durata in minuti (opzionale)",
"completed_at_optional": "Completato il (opzionale, vuoto = adesso)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Titolo pannello laterale",
"settings_notifications": "Notifiche",
"settings_notify_service": "Servizio di notifica",
"settings_shopping_list": "Lista della spesa (attività di acquisto)",
"settings_shopping_list_help": "I promemoria di acquisto per ricambi scarsi appaiono in questo elenco; spuntarne uno rifornisce il ricambio.",
"shopping_list_none": "Disattivato — nessuna lista della spesa",
"settings_install_assist_sentences": "Installa le frasi di Assist",
"settings_install_assist_sentences_hint": "Copia le frasi vocali nella tua configurazione affinché l'agente Assist classico le riconosca. Un file modificato da te non viene mai sovrascritto.",
"test_notification": "Notifica di test",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Aggiungi passo",
"phase_current": "Fase attuale",
"phase_set": "Imposta come attuale",
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)"
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)",
"chart_history_alarm": "Vista del trigger dallo storico stati: 1 = stato di allarme mantenuto per il tempo di attesa, 0 = ok (in genere gli ultimi ~10 giorni)",
"chart_history_count": "Conteggio dei cambi ricostruito dallo storico stati dall'ultimo intervento (in genere gli ultimi ~10 giorni)",
"prediction_cycles": "Appreso da cicli",
"phase_require_override": "Sostituisci «Richiedi al completamento» per questa fase",
"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.",
"require_tag_scan_hint": "Questa attività si completa solo scansionando il suo tag NFC o codice QR sull'oggetto — il salvataggio qui verrà rifiutato."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。",
"at_time": "時刻",
"notes_optional": "メモ(任意)",
"notes_markdown_hint": "Markdownに対応 — **太字**、リスト、[リンク](…)",
"cost_optional": "費用(任意)",
"duration_minutes": "所要時間(分、任意)",
"completed_at_optional": "完了日時(任意・空欄 = 現在)",
@@ -453,6 +454,9 @@
"settings_panel_title": "サイドバーパネルのタイトル",
"settings_notifications": "通知",
"settings_notify_service": "通知サービス",
"settings_shopping_list": "買い物リスト(購入タスク)",
"settings_shopping_list_help": "在庫が少ない部品の購入リマインダーがこのリストに表示され、チェックすると部品が補充されます。",
"shopping_list_none": "オフ — 買い物リストなし",
"settings_install_assist_sentences": "Assist の文を導入する",
"settings_install_assist_sentences_hint": "音声フレーズを設定に複製し、従来の Assist エージェントが認識できるようにします。自分で編集したファイルが上書きされることはありません。",
"test_notification": "テスト通知",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "ステップを追加",
"phase_current": "現在のフェーズ",
"phase_set": "現在に設定",
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)"
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)",
"chart_history_alarm": "状態履歴からのトリガー表示:1 = 保持時間の間アラート状態が続いた、0 = 正常(通常は直近約10日)",
"chart_history_count": "前回のサービス以降の状態履歴から再構築した変化回数(通常は直近約10日)",
"prediction_cycles": "学習したサイクル数",
"phase_require_override": "このフェーズで「完了時に必須」を上書き",
"history_add_past": "過去の完了を追加",
"require_tag_scan": "タグのスキャンでのみ完了可能",
"require_tag_scan_help": "在席証明:対象物のNFCタグまたはQRコードをスキャンするまで、どの画面でも「完了」はブロックされます。オートメーションはcompleteサービスに 'via_tag_scan' を渡せます。",
"require_tag_scan_hint": "このタスクは対象物のNFCタグまたはQRコードをスキャンしないと完了できません。ここでの保存は拒否されます。"
}
@@ -128,6 +128,7 @@
"schedule_time_help": "비워두면 자정(기본값)입니다. HA 시간대 기준.",
"at_time": "시각",
"notes_optional": "메모 (선택)",
"notes_markdown_hint": "Markdown 지원 — **굵게**, 목록, [링크](…)",
"cost_optional": "비용 (선택)",
"duration_minutes": "소요 시간(분, 선택)",
"completed_at_optional": "완료 시각 (선택, 비우면 지금)",
@@ -453,6 +454,9 @@
"settings_panel_title": "사이드바 패널 제목",
"settings_notifications": "알림",
"settings_notify_service": "알림 서비스",
"settings_shopping_list": "쇼핑 목록 (구매 작업)",
"settings_shopping_list_help": "부족한 부품의 구매 알림이 이 목록에 표시되며, 체크하면 부품이 재입고됩니다.",
"shopping_list_none": "끄기 — 쇼핑 목록 없음",
"settings_install_assist_sentences": "Assist 문장 설치",
"settings_install_assist_sentences_hint": "음성 문장을 설정에 복사하여 기존 Assist 에이전트가 인식하도록 합니다. 직접 수정한 파일은 절대 덮어쓰지 않습니다.",
"test_notification": "테스트 알림",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "스텝 추가",
"phase_current": "현재 단계",
"phase_set": "현재로 설정",
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)"
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)",
"chart_history_alarm": "상태 기록 기반 트리거 보기: 1 = 유지 시간 동안 경보 상태 유지, 0 = 정상 (보통 최근 약 10일)",
"chart_history_count": "마지막 서비스 이후 상태 기록으로 재구성한 변경 횟수 (보통 최근 약 10일)",
"prediction_cycles": "학습한 주기 수",
"phase_require_override": "이 단계에서 ‘완료 시 필수’ 재정의",
"history_add_past": "지난 완료 추가",
"require_tag_scan": "태그 스캔으로만 완료 가능",
"require_tag_scan_help": "현장 확인: 기기의 NFC 태그나 QR 코드를 스캔할 때까지 모든 화면에서 '완료'가 차단됩니다. 자동화는 complete 서비스에 'via_tag_scan'을 전달할 수 있습니다.",
"require_tag_scan_hint": "이 작업은 기기의 NFC 태그나 QR 코드를 스캔해야만 완료됩니다. 여기서 저장하면 거부됩니다."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Tom = midnatt (standard). HA-tidssone.",
"at_time": "kl.",
"notes_optional": "Notater (valgfritt)",
"notes_markdown_hint": "Markdown støttes — **fet**, lister, [lenker](…)",
"cost_optional": "Kostnad (valgfritt)",
"duration_minutes": "Varighet i minutter (valgfritt)",
"completed_at_optional": "Utført den (valgfritt, tomt = nå)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Tittel på sidepanel",
"settings_notifications": "Varsler",
"settings_notify_service": "Varslingstjeneste",
"settings_shopping_list": "Handleliste (kjøpsoppgaver)",
"settings_shopping_list_help": "Kjøpspåminnelser for deler med lav beholdning vises i denne listen; avkryssing fyller på beholdningen.",
"shopping_list_none": "Av — ingen handleliste",
"settings_install_assist_sentences": "Installer Assist-setninger",
"settings_install_assist_sentences_hint": "Kopierer talesetningene til konfigurasjonen din slik at den klassiske Assist-agenten gjenkjenner dem. En fil du selv har endret, blir aldri overskrevet.",
"test_notification": "Testvarsel",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Legg til trinn",
"phase_current": "Gjeldende fase",
"phase_set": "Sett som gjeldende",
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)"
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)",
"chart_history_alarm": "Utløservisning fra tilstandshistorikken: 1 = alarmtilstand holdt gjennom holdetiden, 0 = ok (vanligvis de siste ~10 dagene)",
"chart_history_count": "Antall endringer gjenoppbygd fra tilstandshistorikken siden siste service (vanligvis de siste ~10 dagene)",
"prediction_cycles": "Lært av sykluser",
"phase_require_override": "Overstyr «Krev ved fullføring» for denne fasen",
"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.",
"require_tag_scan_hint": "Denne oppgaven fullføres bare ved å skanne NFC-taggen eller QR-koden på selve tingen — lagring her vil bli avvist."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.",
"at_time": "om",
"notes_optional": "Notities (optioneel)",
"notes_markdown_hint": "Markdown wordt ondersteund — **vet**, lijsten, [links](…)",
"cost_optional": "Kosten (optioneel)",
"duration_minutes": "Duur in minuten (optioneel)",
"completed_at_optional": "Voltooid op (optioneel, leeg = nu)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Titel zijbalkpaneel",
"settings_notifications": "Meldingen",
"settings_notify_service": "Meldingsservice",
"settings_shopping_list": "Boodschappenlijst (kooptaken)",
"settings_shopping_list_help": "Koopherinneringen voor schaarse onderdelen verschijnen in deze lijst; afvinken vult de voorraad aan.",
"shopping_list_none": "Uit — geen boodschappenlijst",
"settings_install_assist_sentences": "Assist-zinnen installeren",
"settings_install_assist_sentences_hint": "Kopieert de spraakzinnen naar je configuratie zodat de klassieke Assist-agent ze herkent. Een bestand dat je zelf hebt bewerkt wordt nooit overschreven.",
"test_notification": "Testmelding",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Stap toevoegen",
"phase_current": "Huidige fase",
"phase_set": "Instellen als huidige",
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)"
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)",
"chart_history_alarm": "Triggerweergave uit de statusgeschiedenis: 1 = alarmstatus gedurende de wachttijd aangehouden, 0 = in orde (meestal de laatste ~10 dagen)",
"chart_history_count": "Aantal wisselingen opnieuw opgebouwd uit de statusgeschiedenis sinds de laatste service (meestal de laatste ~10 dagen)",
"prediction_cycles": "Geleerd uit cycli",
"phase_require_override": "„Vereisen bij afronden” voor deze fase overschrijven",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.",
"at_time": "o",
"notes_optional": "Notatki (opcjonalne)",
"notes_markdown_hint": "Obsługiwany jest Markdown — **pogrubienie**, listy, [linki](…)",
"cost_optional": "Koszt (opcjonalne)",
"duration_minutes": "Czas trwania w minutach (opcjonalne)",
"completed_at_optional": "Wykonano dnia (opcjonalne, puste = teraz)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Tytuł panelu bocznego",
"settings_notifications": "Powiadomienia",
"settings_notify_service": "Usługa powiadomień",
"settings_shopping_list": "Lista zakupów (zadania zakupowe)",
"settings_shopping_list_help": "Przypomnienia o zakupie brakujących części pojawiają się na tej liście; odhaczenie uzupełnia zapas.",
"shopping_list_none": "Wyłączone — brak listy zakupów",
"settings_install_assist_sentences": "Zainstaluj zdania Assist",
"settings_install_assist_sentences_hint": "Kopiuje zdania głosowe do Twojej konfiguracji, aby klasyczny agent Assist je rozpoznawał. Plik zmieniony przez Ciebie nigdy nie zostanie nadpisany.",
"test_notification": "Powiadomienie testowe",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Dodaj krok",
"phase_current": "Bieżąca faza",
"phase_set": "Ustaw jako bieżącą",
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)"
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)",
"chart_history_alarm": "Widok wyzwalacza z historii stanów: 1 = stan alarmu utrzymany przez czas podtrzymania, 0 = w porządku (zwykle ostatnie ~10 dni)",
"chart_history_count": "Licznik zmian odtworzony z historii stanów od ostatniego serwisu (zwykle ostatnie ~10 dni)",
"prediction_cycles": "Nauczone z cykli",
"phase_require_override": "Nadpisz „Wymagaj przy ukończeniu” dla tej fazy",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário do HA.",
"at_time": "às",
"notes_optional": "Observações (opcional)",
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [links](…)",
"cost_optional": "Custo (opcional)",
"duration_minutes": "Duração em minutos (opcional)",
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Título do painel na barra lateral",
"settings_notifications": "Notificações",
"settings_notify_service": "Serviço de notificação",
"settings_shopping_list": "Lista de compras (tarefas de compra)",
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
"shopping_list_none": "Desligado — sem lista de compras",
"settings_install_assist_sentences": "Instalar frases do Assist",
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um arquivo que você editou nunca é sobrescrito.",
"test_notification": "Notificação de teste",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Adicionar passo",
"phase_current": "Fase atual",
"phase_set": "Definir como atual",
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)"
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)",
"chart_history_alarm": "Visão do gatilho a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
"prediction_cycles": "Aprendido de ciclos",
"phase_require_override": "Substituir “Exigir na conclusão” para esta fase",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.",
"at_time": "às",
"notes_optional": "Notas (opcional)",
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [ligações](…)",
"cost_optional": "Custo (opcional)",
"duration_minutes": "Duração em minutos (opcional)",
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Título do painel lateral",
"settings_notifications": "Notificações",
"settings_notify_service": "Serviço de notificação",
"settings_shopping_list": "Lista de compras (tarefas de compra)",
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
"shopping_list_none": "Desligado — sem lista de compras",
"settings_install_assist_sentences": "Instalar frases do Assist",
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um ficheiro que editou nunca é substituído.",
"test_notification": "Notificação de teste",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Adicionar passo",
"phase_current": "Fase atual",
"phase_set": "Definir como atual",
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)"
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)",
"chart_history_alarm": "Vista do disparador a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
"prediction_cycles": "Aprendido de ciclos",
"phase_require_override": "Substituir «Exigir na conclusão» para esta fase",
"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.",
"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."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.",
"at_time": "в",
"notes_optional": "Примечания (опционально)",
"notes_markdown_hint": "Поддерживается Markdown — **жирный**, списки, [ссылки](…)",
"cost_optional": "Стоимость (опционально)",
"duration_minutes": "Длительность в минутах (опционально)",
"completed_at_optional": "Выполнено (необязательно, пусто = сейчас)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Заголовок панели",
"settings_notifications": "Уведомления",
"settings_notify_service": "Сервис уведомлений",
"settings_shopping_list": "Список покупок (задачи покупки)",
"settings_shopping_list_help": "Напоминания о покупке заканчивающихся деталей появляются в этом списке; отметка пополняет запас.",
"shopping_list_none": "Выкл. — без списка покупок",
"settings_install_assist_sentences": "Установить фразы Assist",
"settings_install_assist_sentences_hint": "Копирует голосовые фразы в вашу конфигурацию, чтобы классический агент Assist их распознавал. Файл, который вы изменили, никогда не перезаписывается.",
"test_notification": "Тестовое уведомление",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Добавить шаг",
"phase_current": "Текущая фаза",
"phase_set": "Сделать текущей",
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)"
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)",
"chart_history_alarm": "Вид триггера из истории состояний: 1 = состояние тревоги удерживалось в течение времени удержания, 0 = норма (обычно последние ~10 дней)",
"chart_history_count": "Счётчик изменений восстановлен из истории состояний с момента последнего обслуживания (обычно последние ~10 дней)",
"prediction_cycles": "Изучено по циклам",
"phase_require_override": "Переопределить «Требовать при выполнении» для этой фазы",
"history_add_past": "Добавить прошлое выполнение",
"require_tag_scan": "Завершение только по скану метки",
"require_tag_scan_help": "Подтверждение присутствия: «Готово» заблокировано везде, пока не отсканирована NFC-метка или QR-код на самом устройстве. Автоматизации могут передавать 'via_tag_scan' в службу complete.",
"require_tag_scan_hint": "Эта задача завершается только сканированием её NFC-метки или QR-кода на самом устройстве — сохранение здесь будет отклонено."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.",
"at_time": "kl.",
"notes_optional": "Anteckningar (valfritt)",
"notes_markdown_hint": "Markdown stöds — **fetstil**, listor, [länkar](…)",
"cost_optional": "Kostnad (valfritt)",
"duration_minutes": "Varaktighet i minuter (valfritt)",
"completed_at_optional": "Utförd den (valfritt, tomt = nu)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Sidopanelens titel",
"settings_notifications": "Notifikationer",
"settings_notify_service": "Notifikationstjänst",
"settings_shopping_list": "Inköpslista (köpuppgifter)",
"settings_shopping_list_help": "Köppåminnelser för delar som tar slut visas i denna lista; att bocka av en fyller på lagret.",
"shopping_list_none": "Av — ingen inköpslista",
"settings_install_assist_sentences": "Installera Assist-meningar",
"settings_install_assist_sentences_hint": "Kopierar rösmeningarna till din konfiguration så att den klassiska Assist-agenten känner igen dem. En fil du själv har ändrat skrivs aldrig över.",
"test_notification": "Testnotifikation",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Lägg till steg",
"phase_current": "Aktuell fas",
"phase_set": "Ange som aktuell",
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)"
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)",
"chart_history_alarm": "Utlösarvy från tillståndshistoriken: 1 = larmläge hållet under hålltiden, 0 = ok (vanligtvis de senaste ~10 dagarna)",
"chart_history_count": "Antal ändringar återskapat från tillståndshistoriken sedan senaste service (vanligtvis de senaste ~10 dagarna)",
"prediction_cycles": "Lärt av cykler",
"phase_require_override": "Åsidosätt ”Kräv vid slutförande” för denna fas",
"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.",
"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."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "Boş = gece yarısı (varsayılan). HA saat dilimi.",
"at_time": "saat",
"notes_optional": "Notlar (isteğe bağlı)",
"notes_markdown_hint": "Markdown desteklenir — **kalın**, listeler, [bağlantılar](…)",
"cost_optional": "Maliyet (isteğe bağlı)",
"duration_minutes": "Dakika cinsinden süre (isteğe bağlı)",
"completed_at_optional": "Tamamlanma zamanı (isteğe bağlı, boş = şimdi)",
@@ -453,6 +454,9 @@
"settings_panel_title": "Kenar çubuğu paneli başlığı",
"settings_notifications": "Bildirimler",
"settings_notify_service": "Bildirim servisi",
"settings_shopping_list": "Alışveriş listesi (satın alma görevleri)",
"settings_shopping_list_help": "Azalan parçalar için satın alma hatırlatıcıları bu listede görünür; işaretlemek parçayı yeniden stoklar.",
"shopping_list_none": "Kapalı — alışveriş listesi yok",
"settings_install_assist_sentences": "Assist cümlelerini yükle",
"settings_install_assist_sentences_hint": "Sesli komut cümlelerini yapılandırmanıza kopyalar, böylece klasik Assist aracısı bunları tanır. Kendi düzenlediğiniz bir dosyanın üzerine asla yazılmaz.",
"test_notification": "Test bildirimi",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Adım ekle",
"phase_current": "Geçerli aşama",
"phase_set": "Geçerli olarak ayarla",
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)"
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)",
"chart_history_alarm": "Durum geçmişinden tetikleyici görünümü: 1 = uyarı durumu bekletme süresi boyunca sürdü, 0 = normal (genellikle son ~10 gün)",
"chart_history_count": "Değişim sayısı son servisten bu yana durum geçmişinden yeniden oluşturuldu (genellikle son ~10 gün)",
"prediction_cycles": "Döngülerden öğrenildi",
"phase_require_override": "Bu aşama için “Tamamlarken zorunlu tut” ayarını geçersiz kıl",
"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.",
"require_tag_scan_hint": "Bu görev yalnızca cihazın üzerindeki NFC etiketi veya QR kodu taranarak tamamlanır — buradan kaydetme reddedilecektir."
}
@@ -126,6 +126,7 @@
"schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.",
"at_time": "о",
"notes_optional": "Примітки (необов'язково)",
"notes_markdown_hint": "Підтримується Markdown — **жирний**, списки, [посилання](…)",
"cost_optional": "Вартість (необов'язково)",
"duration_minutes": "Тривалість у хвилинах (необов'язково)",
"completed_at_optional": "Виконано (необов'язково, порожньо = зараз)",
@@ -451,6 +452,9 @@
"settings_panel_title": "Заголовок панелі",
"settings_notifications": "Сповіщення",
"settings_notify_service": "Служба сповіщень",
"settings_shopping_list": "Список покупок (завдання покупки)",
"settings_shopping_list_help": "Нагадування про купівлю деталей, що закінчуються, з'являються в цьому списку; відмітка поповнює запас.",
"shopping_list_none": "Вимк. — без списку покупок",
"settings_install_assist_sentences": "Встановити фрази Assist",
"settings_install_assist_sentences_hint": "Копіює голосові фрази до вашої конфігурації, щоб класичний агент Assist їх розпізнавав. Файл, який ви редагували, ніколи не перезаписується.",
"test_notification": "Тестове сповіщення",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "Додати крок",
"phase_current": "Поточна фаза",
"phase_set": "Зробити поточною",
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)"
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)",
"chart_history_alarm": "Вигляд тригера з історії станів: 1 = стан тривоги утримувався протягом часу утримання, 0 = норма (зазвичай останні ~10 днів)",
"chart_history_count": "Лічильник змін відновлено з історії станів від останнього обслуговування (зазвичай останні ~10 днів)",
"prediction_cycles": "Вивчено з циклів",
"phase_require_override": "Перевизначити «Вимагати при виконанні» для цієї фази",
"history_add_past": "Додати минуле виконання",
"require_tag_scan": "Завершення лише скануванням мітки",
"require_tag_scan_help": "Підтвердження присутності: «Готово» заблоковано всюди, доки не відскановано NFC-мітку або QR-код на самому пристрої. Автоматизації можуть передати 'via_tag_scan' службі complete.",
"require_tag_scan_hint": "Це завдання завершується лише скануванням його NFC-мітки або QR-коду на самому пристрої — збереження тут буде відхилено."
}
@@ -128,6 +128,7 @@
"schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。",
"at_time": "于",
"notes_optional": "备注 (可选)",
"notes_markdown_hint": "支持 Markdown — **加粗**、列表、[链接](…)",
"cost_optional": "成本 (可选)",
"duration_minutes": "耗时 (分钟, 可选)",
"completed_at_optional": "完成时间(可选,留空 = 现在)",
@@ -453,6 +454,9 @@
"settings_panel_title": "侧边栏面板标题",
"settings_notifications": "通知",
"settings_notify_service": "通知服务",
"settings_shopping_list": "购物清单(购买任务)",
"settings_shopping_list_help": "库存不足部件的购买提醒会显示在此清单中;勾选即可补充库存。",
"shopping_list_none": "关闭 — 无购物清单",
"settings_install_assist_sentences": "安装 Assist 语句",
"settings_install_assist_sentences_hint": "将语音语句复制到你的配置中,让传统 Assist 代理能够识别它们。你自己修改过的文件永远不会被覆盖。",
"test_notification": "测试通知",
@@ -898,5 +902,13 @@
"phase_sequence_add_step": "添加步骤",
"phase_current": "当前阶段",
"phase_set": "设为当前",
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)"
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)",
"chart_history_alarm": "来自状态历史的触发视图:1 = 警报状态保持了保持时间,0 = 正常(通常为最近约 10 天)",
"chart_history_count": "根据上次维护以来的状态历史重建的变化次数(通常为最近约 10 天)",
"prediction_cycles": "已学习的周期数",
"phase_require_override": "为此阶段覆盖“完成时要求”",
"history_add_past": "补录过去的完成记录",
"require_tag_scan": "仅可通过扫描标签完成",
"require_tag_scan_help": "在场证明:在扫描设备上的 NFC 标签或二维码之前,所有界面的“完成”均被阻止。自动化可向 complete 服务传递 'via_tag_scan'。",
"require_tag_scan_hint": "此任务只能通过扫描设备上的 NFC 标签或二维码来完成——在此保存将被拒绝。"
}
@@ -1,9 +1,9 @@
/*! maintenance_supporter frontend 2.65.0 */
var $t=Object.defineProperty;var ts=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 is=(r,s)=>{for(var e in s)$t(r,e,{get:s[e],enumerable:!0})};var c=(r,s,e,t)=>{for(var i=t>1?void 0:t?ts(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&&$t(s,e,i),i};var je,Ne,Xe,kt,be,Et,S,St,et,tt=w(()=>{je=globalThis,Ne=je.ShadowRoot&&(je.ShadyCSS===void 0||je.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Xe=Symbol(),kt=new WeakMap,be=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(Ne&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=kt.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&kt.set(e,s))}return s}toString(){return this.cssText}},Et=r=>new be(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 be(e,r,Xe)},St=(r,s)=>{if(Ne)r.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=je.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,r.appendChild(t)}},et=Ne?r=>r:r=>r instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return Et(e)})(r):r});var ss,rs,as,ns,os,ls,He,At,cs,ds,xe,we,De,Tt,G,$e=w(()=>{tt();tt();({is:ss,defineProperty:rs,getOwnPropertyDescriptor:as,getOwnPropertyNames:ns,getOwnPropertySymbols:os,getPrototypeOf:ls}=Object),He=globalThis,At=He.trustedTypes,cs=At?At.emptyScript:"",ds=He.reactiveElementPolyfillSupport,xe=(r,s)=>r,we={toAttribute(r,s){switch(s){case Boolean:r=r?cs: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}},De=(r,s)=>!ss(r,s),Tt={attribute:!0,type:String,converter:we,reflect:!1,useDefault:!1,hasChanged:De};Symbol.metadata??=Symbol("metadata"),He.litPropertyMetadata??=new WeakMap;G=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=Tt){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&&rs(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:n}=as(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)??Tt}static _$Ei(){if(this.hasOwnProperty(xe("elementProperties")))return;let s=ls(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(xe("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(xe("properties"))){let e=this.properties,t=[...ns(e),...os(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 St(s,this.constructor.elementStyles),s}connectedCallback(){this.renderRoot??=thiLine truncated
\f\r]`,ke=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Pt=/-->/g,Lt=/>/g,ie=RegExp(`>|${it}(?:([^\\s"'>=/]+)(${it}*=${it}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`,"g"),Rt=/'/g,qt=/"/g,Dt=/^(?:script|style|textarea|title)$/i,dt=r=>(s,...e)=>({_$litType$:r,strings:s,values:e}),l=dt(1),_e=dt(2),vr=dt(3),ae=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),jt=new WeakMap,se=re.createTreeWalker(re,129);hs=(r,s)=>{let e=r.length-1,t=[],i,n=s===2?"<svg>":s===3?"<math>":"",o=ke;for(let p=0;p<e;p++){let d=r[p],u,m,f=-1,v=0;for(;v<d.length&&(o.lastIndex=v,m=o.exec(d),m!==null);)v=o.lastIndex,o===ke?m[1]==="!--"?o=Pt:m[1]!==void 0?o=Lt:m[2]!==void 0?(Dt.test(m[2])&&(i=RegExp("</"+m[2],"g")),o=ie):m[3]!==void 0&&(o=ie):o===ie?m[0]===">"?(o=i??ke,f=-1):m[1]===void 0?f=-2:(f=o.lastIndex-m[2].length,u=m[1],o=m[3]===void 0?ie:m[3]==='"'?qt:Rt):o===qt||o===Rt?o=ie:o===Pt||o===Lt?o=ke:(o=ie,i=void 0);let b=o===ie&&r[p+1].startsWith("/>")?" ":"";n+=o===ke?d+ps:f>=0?(t.push(u),d.slice(0,f)+Nt+d.slice(f)+ee+b):d+ee+(f===-2?p:b)}return[Mt(r,n+(r[e]||"<?>")+(s===2?"</svg>":s===3?"</math>":"")),t]},Ae=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]=hs(s,e);if(this.el=r.createElement(u,t),se.currentNode=this.el.content,e===2||e===3){let f=this.el.content.firstChild;f.replaceWith(...f.childNodes)}for(;(i=se.nextNode())!==null&&d.length<p;){if(i.nodeType===1){if(i.hasAttributes())for(let f of i.getAttributeNames())if(f.endsWith(Nt)){let v=m[o++],b=i.getAttribute(f).split(ee),k=/([.?@])?(.*)/.exec(v);d.push({type:1,index:n,name:k[2],strings:b,ctor:k[1]==="."?rt:k[1]==="?"?at:k[1]==="@"?nt:he}),i.removeAttribute(f)}else f.startsWith(ee)&&(d.push({type:6,index:n}),i.removeAttribute(f));if(Dt.test(i.tagName)){let f=i.textContent.split(ee),v=f.length-1;if(v>0){i.textContent=Me?Me.emptyScript:"";for(let b=0;b<v;b++)i.append(f[b],Ee()),se.nextNode(),d.push({type:2,index:++n});i.append(f[v],Ee())}}}else if(i.nodeType===8)if(i.data===Ht)d.push({type:2,index:n});else{let f=-1;for(;(f=i.data.indexOf(ee,f+1))!==-1;)d.push({type:7,index:n}),f+=ee.length-1}n++}}static createElement(s,e){let t=re.createElement("template");return t.innerHTML=s,t}};st=class{constructor(s,e){this._$AV=[],this._$AN=void 0,this._$AD=s,this._$AM=e}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(s){let{el:{content:e},parts:t}=this._$AD,i=(s?.creationScope??re).importNode(e,!0);se.currentNode=i;let n=se.nextNode(),o=0,p=0,d=t[0];for(;d!==void 0;){if(o===d.index){let u;d.type===2?u=new Te(n,n.nextSibling,this,s):d.type===1?u=new d.ctor(n,d.name,d.strings,this,s):d.type===6&&(u=new ot(n,this,s)),this._$AV.push(u),d=t[++p]}o!==d?.index&&(n=se.nextNode(),o++)}return se.currentNode=re,i}p(s){let e=0;for(let t of this._$AV)t!==void 0&&(t.strings!==void 0?(t._$AI(s,t,e),e+=t.strings.length-2):t._$AI(s[e])),e++}},Te=class r{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(s,e,t,i){this.type=2,this._$AH=_,this._$AN=void 0,this._$AA=s,this._$AB=e,this._$AM=t,this.options=i,this._$Cv=i?.isConnected??!0}get parentNode(){let s=this._$AA.parentNode,e=this._$AM;return e!==void 0&&s?.nodeType===11&&(s=e.parentNode),s}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(s,e=this){s=ue(this,s,e),Se(s)?s===_||s==null||s===""?(this._$AH!==_&&this._$AR(),this._$AH=_):s!==this._$AH&&s!==ae&&this._(s):s._$litType$!==void 0?this.$(s):s.nodeType!==void 0?this.T(s):us(s)?this.k(s):this._(s)}O(s){return this._$AA.parentNode.insertBefore(s,this._$AB)}T(s){this._$AH!==s&&(this._$AR(),this._$AH=this.O(s))}_(s){this._$AH!==_&&Se(this._$AH)?this._$AA.nextSibling.data=s:this.T(re.createTextNode(s)),this._$AH=s}$(s){let{values:e,_$litType$:t}=s,i=typeof t=="number"?this._$AC(s):(t.el===void 0&&(t.el=Ae.createElement(Mt(t.h,t.h[0]),this.options)),t);if(this._$AH?._$AD===i)this._$AH.p(e);else{let n=new st(i,this),o=n.u(this.options);n.p(e),this.T(o),this._$AH=n}}_$AC(s){let e=jt.get(s.strings);return e===void 0&&jt.set(s.strings,e=new Ae(s)),e}k(s){ct(this._$AH)||(this._$AH=[],this._$AR());let e=this._$AH,t,i=0;for(let n of s)i===e.length?e.push(t=new r(this.O(Ee()),this.O(Ee()),this,this.options)):t=e[i],t._$AI(n),i++;i<e.length&&(this._$AR(t&&t._$AB.nextSibling,i),e.length=i)}_$AR(s=this._$AA.nextSibling,e){for(this._$AP?.(!1,!0,e);s!==this._$AB;){let t=Ct(s).nextSibling;Ct(s).remove(),s=t}}setConnected(s){this._$AM===void 0&&(this._$Cv=s,this._$AP?.(s))}},he=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(s,e,t,i,n){this.type=1,this._$AH=_,this._$AN=void 0,this.element=s,this.name=e,this._$AM=i,this.options=n,t.length>2||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=!Se(s)||s!==this._$AH&&s!==ae,o&&(this._$AH=s);else{let p=s,d,u;for(s=n[0],d=0;d<n.length-1;d++)u=ue(this,p[t+d],e,d),u===ae&&(u=this._$AH[d]),o||=!Se(u)||u!==this._$AH[d],u===_?s=_:s!==_&&(s+=(u??"")+n[d+1]),this._$AH[d]=u}o&&!i&&this.j(s)}j(s){s===_?this.element.removeAttribute(this.naLine truncated
/*! maintenance_supporter frontend 2.68.0 */
var St=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)St(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&&St(s,e,i),i};var De,Me,Xe,At,xe,Tt,S,It,et,tt=w(()=>{De=globalThis,Me=De.ShadowRoot&&(De.ShadyCSS===void 0||De.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Xe=Symbol(),At=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(Me&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=At.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&At.set(e,s))}return s}toString(){return this.cssText}},Tt=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)},It=(r,s)=>{if(Me)r.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=De.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,r.appendChild(t)}},et=Me?r=>r:r=>r instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return Tt(e)})(r):r});var ps,us,hs,_s,gs,ms,Oe,Ct,fs,vs,we,$e,Fe,Pt,Y,ke=w(()=>{tt();tt();({is:ps,defineProperty:us,getOwnPropertyDescriptor:hs,getOwnPropertyNames:_s,getOwnPropertySymbols:gs,getPrototypeOf:ms}=Object),Oe=globalThis,Ct=Oe.trustedTypes,fs=Ct?Ct.emptyScript:"",vs=Oe.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),Pt={attribute:!0,type:String,converter:$e,reflect:!1,useDefault:!1,hasChanged:Fe};Symbol.metadata??=Symbol("metadata"),Oe.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=Pt){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)??Pt}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 It(s,this.constructor.elementStyles),s}connectedCallback(){this.renderRoot??=thiLine truncated
\f\r]`,Ee=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Rt=/-->/g,jt=/>/g,re=RegExp(`>|${it}(?:([^\\s"'>=/]+)(${it}*=${it}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`,"g"),Nt=/'/g,Ht=/"/g,Ft=/^(?:script|style|textarea|title)$/i,dt=r=>(s,...e)=>({_$litType$:r,strings:s,values:e}),l=dt(1),_e=dt(2),Er=dt(3),oe=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),Dt=new WeakMap,ae=ne.createTreeWalker(ne,129);xs=(r,s)=>{let e=r.length-1,t=[],i,n=s===2?"<svg>":s===3?"<math>":"",o=Ee;for(let p=0;p<e;p++){let d=r[p],u,m,f=-1,v=0;for(;v<d.length&&(o.lastIndex=v,m=o.exec(d),m!==null);)v=o.lastIndex,o===Ee?m[1]==="!--"?o=Rt:m[1]!==void 0?o=jt:m[2]!==void 0?(Ft.test(m[2])&&(i=RegExp("</"+m[2],"g")),o=re):m[3]!==void 0&&(o=re):o===re?m[0]===">"?(o=i??Ee,f=-1):m[1]===void 0?f=-2:(f=o.lastIndex-m[2].length,u=m[1],o=m[3]===void 0?re:m[3]==='"'?Ht:Nt):o===Ht||o===Nt?o=re:o===Rt||o===jt?o=Ee:(o=re,i=void 0);let y=o===re&&r[p+1].startsWith("/>")?" ":"";n+=o===Ee?d+ys:f>=0?(t.push(u),d.slice(0,f)+Mt+d.slice(f)+ie+y):d+ie+(f===-2?p:y)}return[zt(r,n+(r[e]||"<?>")+(s===2?"</svg>":s===3?"</math>":"")),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<p;){if(i.nodeType===1){if(i.hasAttributes())for(let f of i.getAttributeNames())if(f.endsWith(Mt)){let v=m[o++],y=i.getAttribute(f).split(ie),k=/([.?@])?(.*)/.exec(v);d.push({type:1,index:n,name:k[2],strings:y,ctor:k[1]==="."?rt:k[1]==="?"?at:k[1]==="@"?nt:he}),i.removeAttribute(f)}else f.startsWith(ie)&&(d.push({type:6,index:n}),i.removeAttribute(f));if(Ft.test(i.tagName)){let f=i.textContent.split(ie),v=f.length-1;if(v>0){i.textContent=ze?ze.emptyScript:"";for(let y=0;y<v;y++)i.append(f[y],Se()),ae.nextNode(),d.push({type:2,index:++n});i.append(f[v],Se())}}}else if(i.nodeType===8)if(i.data===Ot)d.push({type:2,index:n});else{let f=-1;for(;(f=i.data.indexOf(ie,f+1))!==-1;)d.push({type:7,index:n}),f+=ie.length-1}n++}}static createElement(s,e){let t=ne.createElement("template");return t.innerHTML=s,t}};st=class{constructor(s,e){this._$AV=[],this._$AN=void 0,this._$AD=s,this._$AM=e}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(s){let{el:{content:e},parts:t}=this._$AD,i=(s?.creationScope??ne).importNode(e,!0);ae.currentNode=i;let n=ae.nextNode(),o=0,p=0,d=t[0];for(;d!==void 0;){if(o===d.index){let u;d.type===2?u=new Ie(n,n.nextSibling,this,s):d.type===1?u=new d.ctor(n,d.name,d.strings,this,s):d.type===6&&(u=new ot(n,this,s)),this._$AV.push(u),d=t[++p]}o!==d?.index&&(n=ae.nextNode(),o++)}return ae.currentNode=ne,i}p(s){let e=0;for(let t of this._$AV)t!==void 0&&(t.strings!==void 0?(t._$AI(s,t,e),e+=t.strings.length-2):t._$AI(s[e])),e++}},Ie=class r{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(s,e,t,i){this.type=2,this._$AH=_,this._$AN=void 0,this._$AA=s,this._$AB=e,this._$AM=t,this.options=i,this._$Cv=i?.isConnected??!0}get parentNode(){let s=this._$AA.parentNode,e=this._$AM;return e!==void 0&&s?.nodeType===11&&(s=e.parentNode),s}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(s,e=this){s=ue(this,s,e),Ae(s)?s===_||s==null||s===""?(this._$AH!==_&&this._$AR(),this._$AH=_):s!==this._$AH&&s!==oe&&this._(s):s._$litType$!==void 0?this.$(s):s.nodeType!==void 0?this.T(s):bs(s)?this.k(s):this._(s)}O(s){return this._$AA.parentNode.insertBefore(s,this._$AB)}T(s){this._$AH!==s&&(this._$AR(),this._$AH=this.O(s))}_(s){this._$AH!==_&&Ae(this._$AH)?this._$AA.nextSibling.data=s:this.T(ne.createTextNode(s)),this._$AH=s}$(s){let{values:e,_$litType$:t}=s,i=typeof t=="number"?this._$AC(s):(t.el===void 0&&(t.el=Te.createElement(zt(t.h,t.h[0]),this.options)),t);if(this._$AH?._$AD===i)this._$AH.p(e);else{let n=new st(i,this),o=n.u(this.options);n.p(e),this.T(o),this._$AH=n}}_$AC(s){let e=Dt.get(s.strings);return e===void 0&&Dt.set(s.strings,e=new Te(s)),e}k(s){ct(this._$AH)||(this._$AH=[],this._$AR());let e=this._$AH,t,i=0;for(let n of s)i===e.length?e.push(t=new r(this.O(Se()),this.O(Se()),this,this.options)):t=e[i],t._$AI(n),i++;i<e.length&&(this._$AR(t&&t._$AB.nextSibling,i),e.length=i)}_$AR(s=this._$AA.nextSibling,e){for(this._$AP?.(!1,!0,e);s!==this._$AB;){let t=Lt(s).nextSibling;Lt(s).remove(),s=t}}setConnected(s){this._$AM===void 0&&(this._$Cv=s,this._$AP?.(s))}},he=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(s,e,t,i,n){this.type=1,this._$AH=_,this._$AN=void 0,this.element=s,this.name=e,this._$AM=i,this.options=n,t.length>2||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<n.length-1;d++)u=ue(this,p[t+d],e,d),u===oe&&(u=this._$AH[d]),o||=!Ae(u)||u!==this._$AH[d],u===_?s=_:s!==_&&(s+=(u??"")+n[d+1]),this._$AH[d]=u}o&&!i&&this.j(s)}j(s){s===_?this.element.removeAttribute(this.naLine truncated
Replace seal
Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"{field}: too long (max {n} characters)",err_too_short:"{field}: too short (min {n} characters)",err_value_too_high:"{field}: too large (max {n})",err_value_too_low:"{field}: too small (min {n})",err_required:"{field}: required",err_wrong_type:"{field}: wrong type (expected: {type})",err_invalid_choice:"{field}: not an allowed value",err_invalid_value:"{field}: invalid value",feat_schedule_time:"Time-of-day scheduling",feat_schedule_time_desc:"Tasks become overdue at a specific time of day instead of midnight.",schedule_time_optional:"Due at time (optional, HH:MM)",schedule_time_help:"Empty = midnight (default). HA timezone.",at_time:"at",notes_optional:"Notes (optional)",cost_optional:"Cost (optional)",duration_minutes:"Duration in minutes (optional)",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",m Line truncated
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",ro Line truncated
.field { display: flex; flex-direction: column; gap: 4px; }
.field-label { font-size: 12px; color: var(--secondary-text-color); }
.field-input {
@@ -14,7 +14,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
font-family: inherit; width: 100%; box-sizing: border-box;
}
.field-input:focus { outline: none; border-color: var(--primary-color); }
`,Ve=S`
`,Ge=S`
:host {
--maint-ok-color: var(--success-color, #4caf50);
--maint-due-soon-color: var(--warning-color, #ff9800);
@@ -945,6 +945,11 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
opacity: 0.7;
}
.prediction-cycles {
font-size: 11px;
opacity: 0.7;
}
.prediction-entity {
font-size: 10px;
opacity: 0.6;
@@ -1144,9 +1149,19 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.stat-item .stat-label { font-size: 11px; white-space: normal; text-align: center; line-height: 1.2; }
.stat-value { font-size: 20px; }
}
`});function Ls(r,s){let e=Ps[r];if(!e)return r;let t=a(e,s);return t&&t!==e?t:r}function Rs(r){let e=r.match(/data\['([^']+)'\]/)?.[1],t;return(t=r.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=r.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=r.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=r.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(r)?{field:e,rule:"required"}:(t=r.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(r)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(r)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function q(r,s,e){if(e=e??a("action_error",s),typeof r=="string")return r;if(typeof r!="object"||r===null)return e;let t=r,i=t.message||t.error?.message||"";if(!i)return e;let n=Rs(i),o=n.field?Ls(n.field,s):"",p=d=>a(d,s).replace("{field}",o).replace("{n}",n.param??"");switch(n.rule){case"too_long":return p("err_too_long");case"too_short":return p("err_too_short");case"value_too_high":return p("err_value_too_high");case"value_too_low":return p("err_value_too_low");case"required":return p("err_required");case"wrong_type":return p("err_wrong_type").replace("{type}",n.param??"");case"invalid_choice":return p("err_invalid_choice");case"invalid_value":return p("err_invalid_value");default:return i||e}}var Ps,le=w(()=>{"use strict";R();Ps={entry_id:"object",name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_equals:"trigger_equals",trigger_not_equals:"trigger_not_equals",trigger_for_minutes:"trigger_for_minutes"}});var N,gt=w(()=>{"use strict";L();U();N=class extends A{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(e){let t=e.target.value;this.value=t,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:t}}))}render(){return l`
`});function Os(r,s){let e=Ms[r];if(!e)return r;let t=a(e,s);return t&&t!==e?t:r}function Fs(r){let e=r.match(/data\['([^']+)'\]/)?.[1],t;return(t=r.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=r.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=r.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=r.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(r)?{field:e,rule:"required"}:(t=r.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(r)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(r)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function N(r,s,e){if(e=e??a("action_error",s),typeof r=="string")return r;if(typeof r!="object"||r===null)return e;let t=r,i=t.message||t.error?.message||"";if(!i)return e;let n=Fs(i),o=n.field?Os(n.field,s):"",p=d=>a(d,s).replace("{field}",o).replace("{n}",n.param??"");switch(n.rule){case"too_long":return p("err_too_long");case"too_short":return p("err_too_short");case"value_too_high":return p("err_value_too_high");case"value_too_low":return p("err_value_too_low");case"required":return p("err_required");case"wrong_type":return p("err_wrong_type").replace("{type}",n.param??"");case"invalid_choice":return p("err_invalid_choice");case"invalid_value":return p("err_invalid_value");default:return i||e}}var Ms,de=w(()=>{"use strict";j();Ms={entry_id:"object",name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_equals:"trigger_equals",trigger_not_equals:"trigger_not_equals",trigger_for_minutes:"trigger_for_minutes"}});var L,gt=w(()=>{"use strict";P();W();L=class extends A{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1;this.multiline=!1;this.rows=3}_onInput(e){let t=e.target.value;this.value=t,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:t}}))}render(){return l`
<label class="field">
${this.label?l`<span class="label">${this.label}${this.required?l`<span class="req">*</span>`:_}</span>`:_}
${this.multiline?l`
<textarea
.value=${this.value??""}
rows=${this.rows}
?required=${this.required}
?disabled=${this.disabled}
placeholder=${this.placeholder}
@input=${this._onInput}
@change=${this._onInput}
></textarea>`:l`
<input
.value=${this.value??""}
.type=${this.type}
@@ -1159,10 +1174,10 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
pattern=${this.pattern??_}
@input=${this._onInput}
@change=${this._onInput}
/>
/>`}
${this.helper?l`<span class="helper">${this.helper}</span>`:_}
</label>
`}};N.styles=S`
`}};L.styles=S`
:host { display: block; }
.field {
display: flex;
@@ -1191,12 +1206,27 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
border-color: var(--primary-color);
}
input:disabled { opacity: 0.5; cursor: not-allowed; }
textarea {
padding: 8px 10px;
font-size: 14px;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
border-radius: 6px;
font-family: inherit;
width: 100%;
box-sizing: border-box;
outline: none;
resize: vertical;
}
textarea:focus { border-color: var(--primary-color); }
textarea:disabled { opacity: 0.5; cursor: not-allowed; }
.helper {
font-size: 11px;
color: var(--secondary-text-color);
font-style: italic;
}
`,c([x()],N.prototype,"label",2),c([x()],N.prototype,"value",2),c([x()],N.prototype,"placeholder",2),c([x()],N.prototype,"type",2),c([x({type:Boolean})],N.prototype,"required",2),c([x({type:Boolean})],N.prototype,"disabled",2),c([x()],N.prototype,"step",2),c([x()],N.prototype,"min",2),c([x()],N.prototype,"max",2),c([x()],N.prototype,"pattern",2),c([x()],N.prototype,"helper",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",N)});var T,yi=w(()=>{"use strict";L();U();R();le();gt();T=class extends A{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return j(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._error="",this._open=!0}openEdit(e,t){this._entryId=e,this._name=t.name||"",this._manufacturer=t.manufacturer||"",this._model=t.model||"",this._serialNumber=t.serial_number||"",this._areaId=t.area_id||"",this._installationDate=t.installation_date||"",this._warrantyExpiry=t.warranty_expiry||"",this._documentationUrl=t.documentation_url||"",this._notes=t.notes||"",this._haDeviceId=t.ha_device_id||"",this._parentEntryId=t.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(e){this._error=q(e,this._lang,a("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(e=>e.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let e=this._lang,t=this._entryId?a("edit_object",e):a("new_object",e);return l`
`,c([x()],L.prototype,"label",2),c([x()],L.prototype,"value",2),c([x()],L.prototype,"placeholder",2),c([x()],L.prototype,"type",2),c([x({type:Boolean})],L.prototype,"required",2),c([x({type:Boolean})],L.prototype,"disabled",2),c([x()],L.prototype,"step",2),c([x()],L.prototype,"min",2),c([x()],L.prototype,"max",2),c([x()],L.prototype,"pattern",2),c([x()],L.prototype,"helper",2),c([x({type:Boolean})],L.prototype,"multiline",2),c([x({type:Number})],L.prototype,"rows",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",L)});var T,$i=w(()=>{"use strict";P();W();j();de();gt();T=class extends A{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return D(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._error="",this._open=!0}openEdit(e,t){this._entryId=e,this._name=t.name||"",this._manufacturer=t.manufacturer||"",this._model=t.model||"",this._serialNumber=t.serial_number||"",this._areaId=t.area_id||"",this._installationDate=t.installation_date||"",this._warrantyExpiry=t.warranty_expiry||"",this._documentationUrl=t.documentation_url||"",this._notes=t.notes||"",this._haDeviceId=t.ha_device_id||"",this._parentEntryId=t.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(e){this._error=N(e,this._lang,a("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(e=>e.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let e=this._lang,t=this._entryId?a("edit_object",e):a("new_object",e);return l`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${t}</div>
<div class="content">
@@ -1276,6 +1306,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.value=${this._notes}
@input=${i=>this._notes=i.target.value}
></textarea>
<span class="md-hint">${a("notes_markdown_hint",e)}</span>
</label>
</div>
<div class="dialog-actions">
@@ -1327,6 +1358,9 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.textarea-field textarea:focus {
outline: none; border-color: var(--primary-color);
}
.md-hint {
font-size: 11px; color: var(--secondary-text-color); font-style: italic;
}
.parent-select {
padding: 8px 10px; font-size: 14px; font-family: inherit;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
@@ -1337,9 +1371,9 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
color: var(--error-color, #f44336);
font-size: 13px;
}
`,c([x({attribute:!1})],T.prototype,"hass",2),c([x({attribute:!1})],T.prototype,"objects",2),c([h()],T.prototype,"_open",2),c([h()],T.prototype,"_loading",2),c([h()],T.prototype,"_error",2),c([h()],T.prototype,"_name",2),c([h()],T.prototype,"_manufacturer",2),c([h()],T.prototype,"_model",2),c([h()],T.prototype,"_serialNumber",2),c([h()],T.prototype,"_areaId",2),c([h()],T.prototype,"_installationDate",2),c([h()],T.prototype,"_warrantyExpiry",2),c([h()],T.prototype,"_documentationUrl",2),c([h()],T.prototype,"_notes",2),c([h()],T.prototype,"_haDeviceId",2),c([h()],T.prototype,"_parentEntryId",2),c([h()],T.prototype,"_entryId",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",T)});var Ke,bi=w(()=>{"use strict";Ke=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=e,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,e,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:e,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}}});function B(r){return`${r.entry_id??""}\0${r.part_id}`}function qs(r,s,e,t){let i=!!r.entry_id&&r.entry_id!==s,n=i?r.entry_id:s,o=e.find(m=>m.entry_id===n),p=(o?.parts||[]).find(m=>m.id===r.part_id)||null,d=i&&o?.object?.name||"",u=p?.name||a("shared_part_unknown",t);return{part:p,foreign:i,ownerName:d,label:d?`${u} (${d})`:u}}function xi(r,s,e,t){let n=(e.find(p=>p.entry_id===s)?.parts||[]).map(p=>({...p})),o=new Set(n.map(p=>B({part_id:p.id})));for(let p of r?.consumes_parts||[]){if(!p.entry_id||p.entry_id===s)continue;let d=B(p);if(o.has(d))continue;o.add(d);let{part:u,ownerName:m}=qs(p,s,e,t);n.push({id:p.part_id,name:u?.name||a("shared_part_unknown",t),unit:u?.unit,stock:u?.stock??null,storage_location:u?.storage_location,entry_id:p.entry_id,owner_name:m})}return n}var Ge=w(()=>{"use strict";R()});var mt,wi,$i,ki=w(()=>{"use strict";mt=["sensor","binary_sensor","number","input_number","input_boolean","switch","climate","vacuum","cover","fan","light","water_heater","humidifier","media_player","weather","air_quality","valve","lawn_mower","lock"],wi=["sensor"],$i=["temperature","humidity","pressure"]});var Ei,Ye,ft=w(()=>{"use strict";Ei=["notes","cost","duration","photo","user"],Ye={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"}});function Ms(){return{entityIds:"",type:"threshold",attribute:"",above:"",below:"",equals:"",notEquals:"",forMinutes:"0",targetValue:"",deltaMode:!1,fromState:"",toState:"",targetChanges:"",runtimeHours:"",onStates:"",carry:{}}}function zs(r){return{entityIds:(r.entity_ids||(r.entity_id?[r.entity_id]:[])).join(", "),type:r.type||"threshold",attribute:r.attribute||"",above:r.trigger_above?.toString()??"",below:r.trigger_below?.toString()??"",equals:r.trigger_equals?.toString()??"",notEquals:r.trigger_not_equals?.toString()??"",forMinutes:r.trigger_for_minutes?.toString()??"0",targetValue:r.trigger_target_value?.toString()??"",deltaMode:r.trigger_delta_mode||!1,fromState:r.trigger_from_state||"",toState:r.trigger_to_state||"",targetChanges:r.trigger_target_changes?.toString()??"",runtimeHours:r.trigger_runtime_hours?.toString()??"",onStates:(r.trigger_on_states||[]).join(", "),carry:Object.fromEntries(Object.entries(r).filter(([e])=>!Os.has(e)&&!e.startsWith("_")))}}function Fs(r){let s=r.entityIds.split(",").map(t=>t.trim()).filter(Boolean);if(s.length===0)return null;let e={...r.carry||{},entity_id:s[0],entity_ids:s,type:r.type};if(r.attribute&&(e.attribute=r.attribute),r.type==="threshold"){let t=parseFloat(r.above);isNaN(t)||(e.trigger_above=t);let i=parseFloat(r.below);isNaN(i)||(e.trigger_below=i);let n=parseFloat(r.equals);isNaN(n)||(e.trigger_equals=n);let o=parseFloat(r.notEquals);isNaN(o)||(e.trigger_not_equals=o);let p=parseInt(r.forMinutes,10);isNaN(p)||(e.trigger_for_minutes=p)}else if(r.type==="counter"){let t=parseFloat(r.targetValue);isNaN(t)||(e.trigger_target_value=t),e.trigger_delta_mode=r.deltaMode}else if(r.type==="state_change"){r.fromState&&(e.trigger_from_state=r.fromState),r.toState&&(e.trigger_to_state=r.toState);let t=parseInt(r.targetChanges,10 Line truncated
`),this._phaseDefs=Object.entries(t.phases||{}).map(([u,m])=>{let{name:f,checklist:v,consumes_parts:b,...k}=m;return{id:u,name:m.name||u,checklistText:(m.checklist||[]).join(`
`),partId:m.consumes_parts?.[0]?.part_id||"",partQty:m.consumes_parts?.[0]?.quantity!=null?String(m.consumes_parts[0].quantity):"",extraParts:(m.consumes_parts||[]).slice(1),carry:k}}),this._phaseSeq=[...t.phase_sequence||[]],this._requiredCompletion=[...t.required_completion_fields||[]],this._scheduleTime=t.schedule_time||"";let o=t.on_complete_action;if(o&&o.service){this._actionService=o.service;let u=o.target?.entity_id;this._actionTargetEntity=Array.isArray(u)?u[0]||"":u||"",this._actionData=o.data&&typeof o.data=="object"?{...o.data}:{},this._actionDataJsonFallback=""}else this._actionService="",this._actionTargetEntity="",this._actionData={},this._actionDataJsonFallback="";let p=t.quick_complete_defaults;this._qcNotes=p?.notes||"",this._qcCost=p?.cost!=null?String(p.cost):"",this._qcDuration=p?.duration!=null?String(p.duration):"",this._qcFeedback=p?.feedback||"";let d=t.adaptive_config||{};if(this._environmentalEntity=d.environmental_entity||"",this._environmentalAttribute=d.environmental_attribute||"",this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute,this._adaptiveEnabled=!!d.enabled,this._adaptiveAlpha=d.ewa_alpha?.toString()??Q.alpha,this._adaptiveMin=d.min_interval_days?.toString()??Q.min,this._adaptiveMax=d.max_interval_days?.toString()??Q.max,this._adaptiveSeasonal=d.seasonal_enabled!==!1,this._adaptivePrediction=d.sensor_prediction_enabled!==!1,this._adaptiveInitial=this._adaptiveSnapshot(),t.trigger_config){let u=t.trigger_config;this._triggerEntityId=u.entity_id||u.entity_ids&&u.entity_ids[0]||"",this._triggerEntityIds=u.entity_ids||(u.entity_id?[u.entity_id]:[]),this._triggerEntityLogic=u.entity_logic||"any",this._triggerAttribute=u.attribute||"",this._triggerType=u.type||"threshold",this._triggerAbove=u.trigger_above?.toString()||"",this._triggerBelow=u.trigger_below?.toString()||"",this._triggerEquals=u.trigger_equals?.toString()||"",this._triggerNotEquals=u.trigger_not_equals?.toString()||"",this._triggerForMinutes=u.trigger_for_minutes?.toString()||"0",this._triggerCombinator=u.trigger_combinator==="all"?"all":"any",this._triggerTargetValue=u.trigger_target_value?.toString()||"",this._triggerDeltaMode=u.trigger_delta_mode||!1,this._triggerBaselineValue=u.trigger_baseline_value?.toString()||"",this._liveBaselineValue=t.trigger_baseline_value??null,this._autoCompleteOnRecovery=u.auto_complete_on_recovery||!1,this._triggerFromState=u.trigger_from_state||"",this._triggerToState=u.trigger_to_state||"",this._triggerTargetChanges=u.trigger_target_changes?.toString()||"",this._triggerRuntimeHours=u.trigger_runtime_hours?.toString()||"",this._triggerOnStates=(u.trigger_on_states||[]).join(", "),u.type==="compound"?(this._compoundLogic=u.compound_logic==="OR"?"OR":"AND",this._compoundConditions=(u.conditions||[]).map(zs)):(this._compoundLogic="AND",this._compoundConditions=[])}else this._resetTriggerFields();this._triggerEntityId&&this._fetchEntityAttributes(this._triggerEntityId),await Promise.all([this._loadUsers(),this._loadTags(),this._loadParts(),this._loadForeignPools()]),this._open=!0}_resetFields(){this._name="",this._type="custom",this._scheduleType="time_based",this._intervalDays="30",this._intervalUnit="days",this._dueDate="",this._warningDays=String(this.defaultWarningDays),this._earliestCompletionDays="",this._intervalAnchor="completion",this._weekdays=[],this._nth="1",this._nthWeekday="5",this._domDay="1",this._domLastDay=!1,this._domBusiness=!1,this._calOffset="0",this._seasonMonths=[],this._endsMode="never",this._endsCount="",this._endsUntil="",this._notes="",this._documentationUrl="",this._customIcon="",this._priority="normal",this._labels="",this._enabled=!0,this._lastPerformed="",this._nfcTagId="",this._readingUnit="",this._consumesParts={},this._responsibleUserId=null,this._assigneePool=[],this._rotationStrategy="",this._checklistText="",this._phaseDefs=[],this._phaseSeq=[],this._requiredCompletion=[],this._scheduleTime="",this._environmentalEntity="",this._environmentalAttribute="",this._environmentalInitial="",this._environmentalAttributeInitial="",this._adaptiveEnabled=!1,this._adaptiveAlpha=Q.alpha,this._adaptiveMin=Q.min,this._adaptiveMax=Q.max,this._adaptiveSeasonal=!0,this._adaptivePrediction=!0,this._adaptiveInitial=this._adaptiveSnapshot(),this._actionService="",this._actionTargetEntity="",this._actionData={},this._actionDataJsonFallback="",this._actionTesting=!1,this._actionTestResult="",this._qcNotes="",this._qcCost="",this._qcDuration="",this._qcFeedback="",this._resetTriggerFields()}_resetTriggerFields(){this._triggerEntityId="",this._triggerEntityIds=[],this._triggerEntityLogic="any",this._triggerAttribute="",this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain="",this._triggerType="threshold",this._triggerAbove="",this._triggerBelow="",this._triggerEquals="",this._triggerNotEquals="",this._triggerForMinutes="0",this._triggerCombinator="any",this._triggerTargetValue="",t Line truncated
`,c([x({attribute:!1})],T.prototype,"hass",2),c([x({attribute:!1})],T.prototype,"objects",2),c([h()],T.prototype,"_open",2),c([h()],T.prototype,"_loading",2),c([h()],T.prototype,"_error",2),c([h()],T.prototype,"_name",2),c([h()],T.prototype,"_manufacturer",2),c([h()],T.prototype,"_model",2),c([h()],T.prototype,"_serialNumber",2),c([h()],T.prototype,"_areaId",2),c([h()],T.prototype,"_installationDate",2),c([h()],T.prototype,"_warrantyExpiry",2),c([h()],T.prototype,"_documentationUrl",2),c([h()],T.prototype,"_notes",2),c([h()],T.prototype,"_haDeviceId",2),c([h()],T.prototype,"_parentEntryId",2),c([h()],T.prototype,"_entryId",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",T)});var Ye,ki=w(()=>{"use strict";Ye=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=e,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,e,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:e,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}}});function V(r){return`${r.entry_id??""}\0${r.part_id}`}function Ei(r,s,e,t){let i=!!r.entry_id&&r.entry_id!==s,n=i?r.entry_id:s,o=e.find(m=>m.entry_id===n),p=(o?.parts||[]).find(m=>m.id===r.part_id)||null,d=i&&o?.object?.name||"",u=p?.name||a("shared_part_unknown",t);return{part:p,foreign:i,ownerName:d,label:d?`${u} (${d})`:u}}function Si(r,s,e,t){let{part:i,label:n}=Ei(r,s,e,t),o=i&&i.stock!==null&&i.stock!==void 0?` (${i.stock}${i.unit?" "+i.unit:""})`:"",p=i?.storage_location?` \u2014 ${i.storage_location}`:"";return`${r.quantity}\xD7 ${n}${o}${p}`}function Ai(r,s,e,t){let n=(e.find(p=>p.entry_id===s)?.parts||[]).map(p=>({...p})),o=new Set(n.map(p=>V({part_id:p.id})));for(let p of r?.consumes_parts||[]){if(!p.entry_id||p.entry_id===s)continue;let d=V(p);if(o.has(d))continue;o.add(d);let{part:u,ownerName:m}=Ei(p,s,e,t);n.push({id:p.part_id,name:u?.name||a("shared_part_unknown",t),unit:u?.unit,stock:u?.stock??null,storage_location:u?.storage_location,entry_id:p.entry_id,owner_name:m})}return n}var Qe=w(()=>{"use strict";j()});var mt,Ti,Ii,Ci=w(()=>{"use strict";mt=["sensor","binary_sensor","number","input_number","input_boolean","switch","climate","vacuum","cover","fan","light","water_heater","humidifier","media_player","weather","air_quality","valve","lawn_mower","lock"],Ti=["sensor"],Ii=["temperature","humidity","pressure"]});var ft,Ne,vt=w(()=>{"use strict";ft=["notes","cost","duration","photo","user"],Ne={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"}});function Vs(){return{entityIds:"",type:"threshold",attribute:"",above:"",below:"",equals:"",notEquals:"",forMinutes:"0",targetValue:"",deltaMode:!1,fromState:"",toState:"",targetChanges:"",runtimeHours:"",onStates:"",carry:{}}}function Gs(r){return{entityIds:(r.entity_ids||(r.entity_id?[r.entity_id]:[])).join(", "),type:r.type||"threshold",attribute:r.attribute||"",above:r.trigger_above?.toString()??"",below:r.trigger_below?.toString()??"",equals:r.trigger_equals?.toString()??"",notEquals:r.trigger_not_equals?.toString()??"",forMinutes:r.trigger_for_minutes?.toString()??"0",targetValue:r.trigger_target_value?.toString()??"",deltaMode:r.trigger_delta_mode||!1,fromState:r.trigger_from_state||"",toState:r.trigger_to_state||"",targetChanges:r.trigger_target_changes?.toString()??"",runtimeHours:r.trigger_runtime_hours?.toString()??"",onStates:(r.trigger_on_states||[]).join(", "),carry:Object.fromEntries(Object.entries(r).filter(([e])=>!Ks.has(e)&&!e.startsWith("_")))}}function Ys(r){let s=r.entityIds.split(",").map(t=>t.trim()).filter(Boolean);if(s.length===0)return null;let e={...r.carry||{},entity_id:s[0],entity_ids:s,type:r.type};if(r.attribute&&(e.attribute=r.attribute),r.type==="threshold"){let t=parseFloat(r.above);isNaN(t)||(e.trigger_above=t);let i=parseFloat(r.below);isNaN(i)||(e.trigger_below=i);let n=parseFloat(r.equals);isNaN(n)||(e.trigger_equals=n);let o=parseFloat(r.notEquals);isNaN(o)||(e.trigger_not_equals=o);let p=parseInt(r.forMinutes,10);isNaN(p)||(e.trigger_for_minutes=p)}else if(r.type==="counter"){let t=parseFloat(r.targetValu Line truncated
`),this._phaseDefs=Object.entries(t.phases||{}).map(([u,m])=>{let{name:f,checklist:v,consumes_parts:y,required_completion_fields:k,...E}=m,H=m.consumes_parts||[],M=H.findIndex(z=>!z.entry_id),q=M>=0?H[M]:void 0;return{id:u,name:m.name||u,checklistText:(m.checklist||[]).join(`
`),partId:q?.part_id||"",partQty:q?.quantity!=null?String(q.quantity):"",reqOverride:m.required_completion_fields!==void 0,reqFields:[...m.required_completion_fields||[]],extraParts:H.filter((z,te)=>te!==M).map(z=>({...z})),carry:E}}),this._phaseSeq=[...t.phase_sequence||[]],this._requiredCompletion=[...t.required_completion_fields||[]],this._scheduleTime=t.schedule_time||"";let o=t.on_complete_action;if(o&&o.service){this._actionService=o.service;let u=o.target?.entity_id;this._actionTargetEntity=Array.isArray(u)?u[0]||"":u||"",this._actionData=o.data&&typeof o.data=="object"?{...o.data}:{},this._actionDataJsonFallback=""}else this._actionService="",this._actionTargetEntity="",this._actionData={},this._actionDataJsonFallback="";let p=t.quick_complete_defaults;this._qcNotes=p?.notes||"",this._qcCost=p?.cost!=null?String(p.cost):"",this._qcDuration=p?.duration!=null?String(p.duration):"",this._qcFeedback=p?.feedback||"";let d=t.adaptive_config||{};if(this._environmentalEntity=d.environmental_entity||"",this._environmentalAttribute=d.environmental_attribute||"",this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute,this._adaptiveEnabled=!!d.enabled,this._adaptiveAlpha=d.ewa_alpha?.toString()??J.alpha,this._adaptiveMin=d.min_interval_days?.toString()??J.min,this._adaptiveMax=d.max_interval_days?.toString()??J.max,this._adaptiveSeasonal=d.seasonal_enabled!==!1,this._adaptivePrediction=d.sensor_prediction_enabled!==!1,this._adaptiveInitial=this._adaptiveSnapshot(),t.trigger_config){let u=t.trigger_config;this._triggerEntityId=u.entity_id||u.entity_ids&&u.entity_ids[0]||"",this._triggerEntityIds=u.entity_ids||(u.entity_id?[u.entity_id]:[]),this._triggerEntityLogic=u.entity_logic||"any",this._triggerAttribute=u.attribute||"",this._triggerType=u.type||"threshold",this._triggerAbove=u.trigger_above?.toString()||"",this._triggerBelow=u.trigger_below?.toString()||"",this._triggerEquals=u.trigger_equals?.toString()||"",this._triggerNotEquals=u.trigger_not_equals?.toString()||"",this._triggerForMinutes=u.trigger_for_minutes?.toString()||"0",this._triggerCombinator=u.trigger_combinator==="all"?"all":"any",this._triggerTargetValue=u.trigger_target_value?.toString()||"",this._triggerDeltaMode=u.trigger_delta_mode||!1,this._triggerBaselineValue=u.trigger_baseline_value?.toString()||"",this._liveBaselineValue=t.trigger_baseline_value??null,this._autoCompleteOnRecovery=u.auto_complete_on_recovery||!1,this._triggerFromState=u.trigger_from_state||"",this._triggerToState=u.trigger_to_state||"",this._triggerTargetChanges=u.trigger_target_changes?.toString()||"",this._triggerRuntimeHours=u.trigger_runtime_hours?.toString()||"",this._triggerOnStates=(u.trigger_on_states||[]).join(", "),u.type==="compound"?(this._compoundLogic=u.compound_logic==="OR"?"OR":"AND",this._compoundConditions=(u.conditions||[]).map(Gs)):(this._compoundLogic="AND",this._compoundConditions=[])}else this._resetTriggerFields();this._triggerEntityId&&this._fetchEntityAttributes(this._triggerEntityId),await Promise.all([this._loadUsers(),this._loadTags(),this._loadParts(),this._loadForeignPools()]),this._open=!0}_resetFields(){this._name="",this._type="custom",this._scheduleType="time_based",this._intervalDays="30",this._intervalUnit="days",this._dueDate="",this._warningDays=String(this.defaultWarningDays),this._earliestCompletionDays="",this._intervalAnchor="completion",this._weekdays=[],this._nth="1",this._nthWeekday="5",this._domDay="1",this._domLastDay=!1,this._domBusiness=!1,this._calOffset="0",this._seasonMonths=[],this._endsMode="never",this._endsCount="",this._endsUntil="",this._notes="",this._documentationUrl="",this._customIcon="",this._priority="normal",this._labels="",this._enabled=!0,this._lastPerformed="",this._nfcTagId="",this._requireTagScan=!1,this._readingUnit="",this._consumesParts={},this._responsibleUserId=null,this._assigneePool=[],this._rotationStrategy="",this._checklistText="",this._phaseDefs=[],this._phaseSeq=[],this._requiredCompletion=[],this._scheduleTime="",this._environmentalEntity="",this._environmentalAttribute="",this._environmentalInitial="",this._environmentalAttributeInitial="",this._adaptiveEnabled=!1,this._adaptiveAlpha=J.alpha,this._adaptiveMin=J.min,this._adaptiveMax=J.max,this._adaptiveSeasonal=!0,this._adaptivePrediction=!0,this._adaptiveInitial=this._adaptiveSnapshot(),this._actionService="",this._actionTargetEntity="",this._actionData={},this._actionDataJsonFallback="",this._actionTesting=!1,this._actionTestResult="",this._qcNotes="",this._qcCost="",this._qcDuration="",this._qcFeedback="",this._resetTriggerFields()}_resetTriggerFields(){this._triggerEntityId="",this._triggerEntityIds=[],this._triggerEntityLogic="any",this._triggerAttribute="",this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain="",this._triggerType="threshold",this._triggerAbove="",this._triggerBelow="",this._triggerEquals="",this._triggerNotEquals="",this._trigger Line truncated
<details class="ca-section">
<summary>${a("on_complete_action_title",e)}</summary>
<p class="field-help">${a("on_complete_action_desc",e)}</p>
@@ -1415,7 +1449,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
<option value="not_needed">${a("quick_complete_defaults_feedback_not_needed",e)}</option>
</select>
</details>
`}async _loadParts(){if(this.parts=[],!!this._entryId)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId});this.parts=e.parts||[],this._partsLoadFailed=!1}catch{this.parts=[],this._partsLoadFailed=!0}}async _loadForeignPools(){if(this._foreignOwners=[],!!this._entryId)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"});this._foreignOwners=(e.objects||[]).filter(t=>t.entry_id!==this._entryId&&(t.parts||[]).length>0).map(t=>({entry_id:t.entry_id,name:t.object?.name||t.entry_id,parts:t.parts||[]})).sort((t,i)=>t.name.localeCompare(i.name))}catch{this._foreignOwners=[]}}async _loadTags(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tags/list"});this._availableTags=e.tags||[]}catch{this._availableTags=[]}}_fetchConditionAttributes(e){!e||!this.hass||this._conditionAttrOptions[e]||this._conditionAttrPending.has(e)||(this._conditionAttrPending.add(e),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/entity/attributes",entity_id:e}).then(t=>{let i=t;this._conditionAttrOptions={...this._conditionAttrOptions,[e]:{suggested:i.suggested_attributes||[],available:i.available_attributes||[]}}}).catch(()=>{this._conditionAttrOptions={...this._conditionAttrOptions,[e]:{suggested:[],available:[]}}}))}async _fetchEntityAttributes(e){if(!e||!this.hass){this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain="";return}try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/entity/attributes",entity_id:e});this._entityDomain=t.domain||"",this._suggestedAttributes=t.suggested_attributes||[],this._availableAttributes=t.available_attributes||[]}catch{this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain=""}}get _hasForeignPick(){return Object.values(this._consumesParts).some(e=>!!e.entry_id)}_renderConsumesRow(e,t){let i=B({part_id:e.id,entry_id:t}),n=this._consumesParts[i],o=t?{part_id:e.id,quantity:1,entry_id:t}:{part_id:e.id,quantity:1};return l`
`}async _loadParts(){if(this.parts=[],!!this._entryId)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId});this.parts=e.parts||[],this._partsLoadFailed=!1}catch{this.parts=[],this._partsLoadFailed=!0}}async _loadForeignPools(){if(this._foreignOwners=[],!!this._entryId)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"});this._foreignOwners=(e.objects||[]).filter(t=>t.entry_id!==this._entryId&&(t.parts||[]).length>0).map(t=>({entry_id:t.entry_id,name:t.object?.name||t.entry_id,parts:t.parts||[]})).sort((t,i)=>t.name.localeCompare(i.name))}catch{this._foreignOwners=[]}}async _loadTags(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tags/list"});this._availableTags=e.tags||[]}catch{this._availableTags=[]}}_fetchConditionAttributes(e){!e||!this.hass||this._conditionAttrOptions[e]||this._conditionAttrPending.has(e)||(this._conditionAttrPending.add(e),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/entity/attributes",entity_id:e}).then(t=>{let i=t;this._conditionAttrOptions={...this._conditionAttrOptions,[e]:{suggested:i.suggested_attributes||[],available:i.available_attributes||[]}}}).catch(()=>{this._conditionAttrOptions={...this._conditionAttrOptions,[e]:{suggested:[],available:[]}}}))}async _fetchEntityAttributes(e){if(!e||!this.hass){this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain="";return}try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/entity/attributes",entity_id:e});this._entityDomain=t.domain||"",this._suggestedAttributes=t.suggested_attributes||[],this._availableAttributes=t.available_attributes||[]}catch{this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain=""}}get _hasForeignPick(){return Object.values(this._consumesParts).some(e=>!!e.entry_id)}_renderConsumesRow(e,t){let i=V({part_id:e.id,entry_id:t}),n=this._consumesParts[i],o=t?{part_id:e.id,quantity:1,entry_id:t}:{part_id:e.id,quantity:1};return l`
<div class="consumes-row">
<label class="consumes-check">
<input
@@ -1435,7 +1469,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
@input=${p=>{let d=parseFloat(p.target.value);this._consumesParts={...this._consumesParts,[i]:{...o,quantity:Number.isFinite(d)&&d>=.01?d:1}}}}
/>`:_}
</div>
`}_toggleRequired(e,t){let i=new Set(this._requiredCompletion);t?i.add(e):i.delete(e),this._requiredCompletion=[...i]}_phaseSlug(e){let t=e.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"phase",i=t,n=2;for(;this._phaseDefs.some(o=>o.id===i);)i=`${t}-${n++}`;return i}_addPhaseDef(){let e=this._phaseSlug(`phase-${this._phaseDefs.length+1}`);this._phaseDefs=[...this._phaseDefs,{id:e,name:"",checklistText:"",partId:"",partQty:"",extraParts:[],carry:{}}]}_removePhaseDef(e){this._phaseDefs=this._phaseDefs.filter(t=>t.id!==e),this._phaseSeq=this._phaseSeq.filter(t=>t!==e)}_patchPhaseDef(e,t){this._phaseDefs=this._phaseDefs.map(i=>i.id===e?{...i,...t}:i)}_renderPhasesEditor(e){let t=i=>this._phaseDefs.find(n=>n.id===i)?.name||i;return l`
`}_toggleRequired(e,t){let i=new Set(this._requiredCompletion);t?i.add(e):i.delete(e),this._requiredCompletion=[...i]}_phaseSlug(e){let t=e.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"phase",i=t,n=2;for(;this._phaseDefs.some(o=>o.id===i);)i=`${t}-${n++}`;return i}_addPhaseDef(){let e=this._phaseSlug(`phase-${this._phaseDefs.length+1}`);this._phaseDefs=[...this._phaseDefs,{id:e,name:"",checklistText:"",partId:"",partQty:"",reqOverride:!1,reqFields:[],extraParts:[],carry:{}}]}_removePhaseDef(e){this._phaseDefs=this._phaseDefs.filter(t=>t.id!==e),this._phaseSeq=this._phaseSeq.filter(t=>t!==e)}_patchPhaseDef(e,t){this._phaseDefs=this._phaseDefs.map(i=>i.id===e?{...i,...t}:i)}_renderPhasesEditor(e){let t=i=>this._phaseDefs.find(n=>n.id===i)?.name||i;return l`
<h3>${a("phases_section",e)}</h3>
<div class="field-help">${a("phases_hint",e)}</div>
${this._phaseDefs.map(i=>l`
@@ -1473,6 +1507,28 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
@input=${n=>this._patchPhaseDef(i.id,{checklistText:n.target.value})}
></textarea>
`:_}
<label class="req-option phase-req-toggle">
<input
type="checkbox"
.checked=${i.reqOverride}
@change=${n=>this._patchPhaseDef(i.id,{reqOverride:n.target.checked})}
/>
<span>${a("phase_require_override",e)}</span>
</label>
${i.reqOverride?l`
<div class="required-completion phase-req-fields">
${ft.map(n=>l`
<label class="req-option">
<input
type="checkbox"
.checked=${i.reqFields.includes(n)}
@change=${o=>{let p=o.target.checked,d=new Set(i.reqFields);p?d.add(n):d.delete(n),this._patchPhaseDef(i.id,{reqFields:[...d]})}}
/>
<span>${a(Ne[n],e)}</span>
</label>
`)}
</div>
`:_}
</div>
`)}
<ha-button appearance="plain" @click=${this._addPhaseDef}>
@@ -1497,9 +1553,9 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
</select>
</div>
`:_}
`}async _save(){if(!this._loading&&this._name.trim()){if(this._adaptiveSnapshot()!==this._adaptiveInitial){let e=parseInt(this._adaptiveMin,10),t=parseInt(this._adaptiveMax,10);if(!isNaN(e)&&!isNaN(t)&&e>t){this._error=`${a("adaptive_min_interval",this._lang)} > ${a("adaptive_max_interval",this._lang)}`;return}}this._loading=!0,this._error="";try{let e={type:this._taskId?"maintenance_supporter/task/update":"maintenance_supporter/task/create",entry_id:this._entryId,name:this._name,task_type:this._type,schedule_type:this._scheduleType,warning_days:Number.isNaN(parseInt(this._warningDays,10))?this.defaultWarningDays:Math.max(0,parseInt(this._warningDays,10))},t=this._earliestCompletionDays.trim();e.earliest_completion_days=t===""?null:Math.max(0,parseInt(t,10)||0),this._taskId&&(e.task_id=this._taskId),this._scheduleType==="one_time"?(e.due_date=this._dueDate||null,e.interval_days=null):vt.includes(this._scheduleType)?(e.schedule={...this._buildSchedule(),...this._recurrenceExtras()},e.interval_days=null,this._taskId&&(e.due_date=null)):(this._taskId&&(e.due_date=null),this._scheduleType!=="manual"&&this._intervalDays?(e.interval_days=parseInt(this._intervalDays,10),e.interval_unit=this._intervalUnit,e.interval_anchor=this._intervalAnchor,this._scheduleType==="time_based"&&(e.schedule={kind:"interval",...this._recurrenceExtras()})):this._taskId&&(e.interval_days=null,e.interval_anchor="completion")),e.notes=this._notes||null,e.documentation_url=this._documentationUrl||null,e.custom_icon=this._customIcon||null,e.priority=this._priority,e.labels=this._labels.split(",").map(p=>p.trim()).filter(Boolean),e.enabled=this._enabled,e.last_performed=this._lastPerformed||null,e.nfc_tag_id=this._nfcTagId||null,e.reading_unit=this._readingUnit.trim()||null;{let p={};for(let u of this._phaseDefs){if(!u.name.trim())continue;let m={...u.carry,name:u.name.trim()},f=u.checklistText.split(`
`).map(b=>b.trim()).filter(Boolean);f.length&&(m.checklist=f);let v=[];if(u.partId){let b=parseFloat(u.partQty);v.push({part_id:u.partId,quantity:Number.isFinite(b)&&b>0?b:1})}v.push(...u.extraParts),v.length&&(m.consumes_parts=v),p[u.id]=m}let d=this._phaseSeq.filter(u=>u in p);e.phases=Object.keys(p).length&&d.length?p:null,e.phase_sequence=e.phases?d:null}if((this.parts.length||this._foreignOwners.length)&&(e.consumes_parts=Object.values(this._consumesParts).map(p=>p.entry_id?{part_id:p.part_id,quantity:p.quantity,entry_id:p.entry_id}:{part_id:p.part_id,quantity:p.quantity})),e.responsible_user_id=this._responsibleUserId,e.assignee_pool=this._assigneePool,e.required_completion_fields=this._requiredCompletion,e.rotation_strategy=this._assigneePool.length>=2&&this._rotationStrategy?this._rotationStrategy:null,this._scheduleType==="sensor_based"&&this._triggerType==="compound"){let p=this._compoundConditions.map(Fs).filter(d=>d!==null);if(p.length>0){let d={type:"compound",compound_logic:this._compoundLogic,conditions:p};this._autoCompleteOnRecovery&&(d.auto_complete_on_recovery=!0),this._triggerCombinator==="all"&&(d.trigger_combinator="all"),e.trigger_config=d}else this._taskId&&(e.trigger_config=null)}else if(this._scheduleType==="sensor_based"&&this._triggerEntityId){let p=this._triggerEntityIds.length>0?this._triggerEntityIds:[this._triggerEntityId],d={entity_id:p[0],entity_ids:p,type:this._triggerType};if(this._triggerAttribute&&(d.attribute=this._triggerAttribute),this._autoCompleteOnRecovery&&(d.auto_complete_on_recovery=!0),this._triggerCombinator==="all"&&(d.trigger_combinator="all"),p.length>1&&(d.entity_logic=this._triggerEntityLogic),this._triggerType==="threshold"){if(this._triggerAbove){let u=parseFloat(this._triggerAbove);isNaN(u)||(d.trigger_above=u)}if(this._triggerBelow){let u=parseFloat(this._triggerBelow);isNaN(u)||(d.trigger_below=u)}if(this._triggerEquals){let u=parseFloat(this._triggerEquals);isNaN(u)||(d.trigger_equals=u)}if(this._triggerNotEquals){let u=parseFloat(this._triggerNotEquals);isNaN(u)||(d.trigger_not_equals=u)}if(this._triggerForMinutes){let u=parseInt(this._triggerForMinutes,10);isNaN(u)||(d.trigger_for_minutes=u)}}else if(this._triggerType==="counter"){if(this._triggerTargetValue){let u=parseFloat(this._triggerTargetValue);isNaN(u)||(d.trigger_target_value=u)}if(d.trigger_delta_mode=this._triggerDeltaMode,this._triggerDeltaMode&&this._triggerBaselineValue){let u=parseFloat(this._triggerBaselineValue);!isNaN(u)&&u>=0&&(d.trigger_baseline_value=u)}}else if(this._triggerType==="state_change"){if(this._triggerFromState&&(d.trigger_from_state=this._triggerFromState),this._triggerToState&&(d.trigger_to_state=this._triggerToState),this._triggerTargetChanges){let u=parseInt(this._triggerTargetChanges,10);isNaN(u)||(d.trigger_target_changes=u)}if(this._triggerForMinutes){let u=parseInt(this._triggerForMinutes,10);isNaN(u)||(d.trigger_for_minutes=u)}}else if(this._triggerType==="runtime"){if(this._triggerRuntimeHours){let m=parseFloat(this._triggerRuntimeHours);isNaN(m)||(d.trigger_runtime_hours=m)}let u=this._triggerOnStates.split(",").map(m=>m.trim()).filter(Boolean);u.length>0&&(d.trigger_on_states=u)}e.trigger_config=d}else this._taskId&&(e.trigger_config=null);if(this.scheduleTimeEnabled&&this._scheduleType==="time_based"){let p=this._scheduleTime.trim();e.schedule_time=/^([01]\d|2[0-3]):[0-5]\d$/.test(p)?p:null}if(this.checklistsEnabled){let p=this._checklistText.split(`
`).map(d=>d.trim()).filter(Boolean).slice(0,100);e.checklist=p.length?p:null}if(this.completionActionsEnabled){let p=this._actionService.trim();if(p&&/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(p)){let f={service:p},v=this._actionTargetEntity.trim();v&&(f.target={entity_id:v});let b=this._buildActionData();Object.keys(b).length>0&&(f.data=b),e.on_complete_action=f}else e.on_complete_action=null;let d={};this._qcNotes.trim()&&(d.notes=this._qcNotes.trim());let u=parseFloat(this._qcCost);!isNaN(u)&&u>=0&&(d.cost=u);let m=parseInt(this._qcDuration,10);!isNaN(m)&&m>=0&&(d.duration=m),this._qcFeedback&&(d.feedback=this._qcFeedback),e.quick_complete_defaults=Object.keys(d).length?d:null}let i=await this.hass.connection.sendMessagePromise(e),n=this._taskId||i?.task_id,o=this._environmentalEntity!==this._environmentalInitial||this._environmentalAttribute!==this._environmentalAttributeInitial;if(n&&this._scheduleType==="sensor_based"&&o)try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/set_environmental_entity",entry_id:this._entryId,task_id:n,environmental_entity:this._environmentalEntity||null,environmental_attribute:this._environmentalAttribute||null}),this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute}catch{}if(n&&this._adaptiveSnapshot()!==this._adaptiveInitial){let p=parseFloat(this._adaptiveAlpha),d=parseInt(this._adaptiveMin,10),u=parseInt(this._adaptiveMax,10);try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/set_adaptive",entry_id:this._entryId,task_id:n,enabled:this._adaptiveEnabled,...p>=.1&&p<=.9?{ewa_alpha:p}:{},...!isNaN(d)&&d>=1?{min_interval_days:d}:{},...!isNaN(u)&&u>=1?{max_interval_days:u}:{},seasonal_enabled:this._adaptiveSeasonal,sensor_prediction_enabled:this._adaptivePrediction}),this._adaptiveInitial=this._adaptiveSnapshot()}catch{}}this._open=!1,this.dispatchEvent(new CustomEvent("task-saved"))}catch(e){this._error=q(e,this._lang,a("save_error",this._lang))}finally{this._loading=!1}}}_close(){this._open=!1,this._pickerProbeTimer!==void 0&&(clearTimeout(this._pickerProbeTimer),this._pickerProbeTimer=void 0),this._pickerProbeStrikes=0}_renderTriggerFields(){if(this._scheduleType!=="sensor_based")return _;let e=this._lang,t=this._triggerType==="compound";return l`
`}async _save(){if(!this._loading&&this._name.trim()){if(this._adaptiveSnapshot()!==this._adaptiveInitial){let e=parseInt(this._adaptiveMin,10),t=parseInt(this._adaptiveMax,10);if(!isNaN(e)&&!isNaN(t)&&e>t){this._error=`${a("adaptive_min_interval",this._lang)} > ${a("adaptive_max_interval",this._lang)}`;return}}this._loading=!0,this._error="";try{let e={type:this._taskId?"maintenance_supporter/task/update":"maintenance_supporter/task/create",entry_id:this._entryId,name:this._name,task_type:this._type,schedule_type:this._scheduleType,warning_days:Number.isNaN(parseInt(this._warningDays,10))?this.defaultWarningDays:Math.max(0,parseInt(this._warningDays,10))},t=this._earliestCompletionDays.trim();e.earliest_completion_days=t===""?null:Math.max(0,parseInt(t,10)||0),this._taskId&&(e.task_id=this._taskId),this._scheduleType==="one_time"?(e.due_date=this._dueDate||null,e.interval_days=null):yt.includes(this._scheduleType)?(e.schedule={...this._buildSchedule(),...this._recurrenceExtras()},e.interval_days=null,this._taskId&&(e.due_date=null)):(this._taskId&&(e.due_date=null),this._scheduleType!=="manual"&&this._intervalDays?(e.interval_days=parseInt(this._intervalDays,10),e.interval_unit=this._intervalUnit,e.interval_anchor=this._intervalAnchor,this._scheduleType==="time_based"&&(e.schedule={kind:"interval",...this._recurrenceExtras()})):this._taskId&&(e.interval_days=null,e.interval_anchor="completion")),e.notes=this._notes||null,e.documentation_url=this._documentationUrl||null,e.custom_icon=this._customIcon||null,e.priority=this._priority,e.labels=this._labels.split(",").map(p=>p.trim()).filter(Boolean),e.enabled=this._enabled,e.last_performed=this._lastPerformed||null,e.nfc_tag_id=this._nfcTagId||null,e.require_tag_scan=this._requireTagScan,e.reading_unit=this._readingUnit.trim()||null;{let p={};for(let u of this._phaseDefs){if(!u.name.trim())continue;let m={...u.carry,name:u.name.trim()},f=u.checklistText.split(`
`).map(y=>y.trim()).filter(Boolean);f.length&&(m.checklist=f);let v=[];if(u.partId){let y=parseFloat(u.partQty);v.push({part_id:u.partId,quantity:Number.isFinite(y)&&y>0?y:1})}for(let y of u.extraParts)v.push(y.entry_id?{part_id:y.part_id,quantity:y.quantity,entry_id:y.entry_id}:{part_id:y.part_id,quantity:y.quantity});v.length&&(m.consumes_parts=v),u.reqOverride&&(m.required_completion_fields=[...u.reqFields]),p[u.id]=m}let d=this._phaseSeq.filter(u=>u in p);e.phases=Object.keys(p).length&&d.length?p:null,e.phase_sequence=e.phases?d:null}if((this.parts.length||this._foreignOwners.length)&&(e.consumes_parts=Object.values(this._consumesParts).map(p=>p.entry_id?{part_id:p.part_id,quantity:p.quantity,entry_id:p.entry_id}:{part_id:p.part_id,quantity:p.quantity})),e.responsible_user_id=this._responsibleUserId,e.assignee_pool=this._assigneePool,e.required_completion_fields=this._requiredCompletion,e.rotation_strategy=this._assigneePool.length>=2&&this._rotationStrategy?this._rotationStrategy:null,this._scheduleType==="sensor_based"&&this._triggerType==="compound"){let p=this._compoundConditions.map(Ys).filter(d=>d!==null);if(p.length>0){let d={type:"compound",compound_logic:this._compoundLogic,conditions:p};this._autoCompleteOnRecovery&&(d.auto_complete_on_recovery=!0),this._triggerCombinator==="all"&&(d.trigger_combinator="all"),e.trigger_config=d}else this._taskId&&(e.trigger_config=null)}else if(this._scheduleType==="sensor_based"&&this._triggerEntityId){let p=this._triggerEntityIds.length>0?this._triggerEntityIds:[this._triggerEntityId],d={entity_id:p[0],entity_ids:p,type:this._triggerType};if(this._triggerAttribute&&(d.attribute=this._triggerAttribute),this._autoCompleteOnRecovery&&(d.auto_complete_on_recovery=!0),this._triggerCombinator==="all"&&(d.trigger_combinator="all"),p.length>1&&(d.entity_logic=this._triggerEntityLogic),this._triggerType==="threshold"){if(this._triggerAbove){let u=parseFloat(this._triggerAbove);isNaN(u)||(d.trigger_above=u)}if(this._triggerBelow){let u=parseFloat(this._triggerBelow);isNaN(u)||(d.trigger_below=u)}if(this._triggerEquals){let u=parseFloat(this._triggerEquals);isNaN(u)||(d.trigger_equals=u)}if(this._triggerNotEquals){let u=parseFloat(this._triggerNotEquals);isNaN(u)||(d.trigger_not_equals=u)}if(this._triggerForMinutes){let u=parseInt(this._triggerForMinutes,10);isNaN(u)||(d.trigger_for_minutes=u)}}else if(this._triggerType==="counter"){if(this._triggerTargetValue){let u=parseFloat(this._triggerTargetValue);isNaN(u)||(d.trigger_target_value=u)}if(d.trigger_delta_mode=this._triggerDeltaMode,this._triggerDeltaMode&&this._triggerBaselineValue){let u=parseFloat(this._triggerBaselineValue);!isNaN(u)&&u>=0&&(d.trigger_baseline_value=u)}}else if(this._triggerType==="state_change"){if(this._triggerFromState&&(d.trigger_from_state=this._triggerFromState),this._triggerToState&&(d.trigger_to_state=this._triggerToState),this._triggerTargetChanges){let u=parseInt(this._triggerTargetChanges,10);isNaN(u)||(d.trigger_target_changes=u)}if(this._triggerForMinutes){let u=parseInt(this._triggerForMinutes,10);isNaN(u)||(d.trigger_for_minutes=u)}}else if(this._triggerType==="runtime"){if(this._triggerRuntimeHours){let m=parseFloat(this._triggerRuntimeHours);isNaN(m)||(d.trigger_runtime_hours=m)}let u=this._triggerOnStates.split(",").map(m=>m.trim()).filter(Boolean);u.length>0&&(d.trigger_on_states=u)}e.trigger_config=d}else this._taskId&&(e.trigger_config=null);if(this.scheduleTimeEnabled&&this._scheduleType==="time_based"){let p=this._scheduleTime.trim();e.schedule_time=/^([01]\d|2[0-3]):[0-5]\d$/.test(p)?p:null}if(this.checklistsEnabled){let p=this._checklistText.split(`
`).map(d=>d.trim()).filter(Boolean).slice(0,100);e.checklist=p.length?p:null}if(this.completionActionsEnabled){let p=this._actionService.trim();if(p&&/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(p)){let f={service:p},v=this._actionTargetEntity.trim();v&&(f.target={entity_id:v});let y=this._buildActionData();Object.keys(y).length>0&&(f.data=y),e.on_complete_action=f}else e.on_complete_action=null;let d={};this._qcNotes.trim()&&(d.notes=this._qcNotes.trim());let u=parseFloat(this._qcCost);!isNaN(u)&&u>=0&&(d.cost=u);let m=parseInt(this._qcDuration,10);!isNaN(m)&&m>=0&&(d.duration=m),this._qcFeedback&&(d.feedback=this._qcFeedback),e.quick_complete_defaults=Object.keys(d).length?d:null}let i=await this.hass.connection.sendMessagePromise(e),n=this._taskId||i?.task_id,o=this._environmentalEntity!==this._environmentalInitial||this._environmentalAttribute!==this._environmentalAttributeInitial;if(n&&this._scheduleType==="sensor_based"&&o)try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/set_environmental_entity",entry_id:this._entryId,task_id:n,environmental_entity:this._environmentalEntity||null,environmental_attribute:this._environmentalAttribute||null}),this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute}catch{}if(n&&this._adaptiveSnapshot()!==this._adaptiveInitial){let p=parseFloat(this._adaptiveAlpha),d=parseInt(this._adaptiveMin,10),u=parseInt(this._adaptiveMax,10);try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/set_adaptive",entry_id:this._entryId,task_id:n,enabled:this._adaptiveEnabled,...p>=.1&&p<=.9?{ewa_alpha:p}:{},...!isNaN(d)&&d>=1?{min_interval_days:d}:{},...!isNaN(u)&&u>=1?{max_interval_days:u}:{},seasonal_enabled:this._adaptiveSeasonal,sensor_prediction_enabled:this._adaptivePrediction}),this._adaptiveInitial=this._adaptiveSnapshot()}catch{}}this._open=!1,this.dispatchEvent(new CustomEvent("task-saved"))}catch(e){this._error=N(e,this._lang,a("save_error",this._lang))}finally{this._loading=!1}}}_close(){this._open=!1,this._pickerProbeTimer!==void 0&&(clearTimeout(this._pickerProbeTimer),this._pickerProbeTimer=void 0),this._pickerProbeStrikes=0}_renderTriggerFields(){if(this._scheduleType!=="sensor_based")return _;let e=this._lang,t=this._triggerType==="compound";return l`
<h3>${a("trigger_configuration",e)}</h3>
<div class="select-row">
<label>${a("trigger_type",e)}</label>
@@ -1507,7 +1563,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.value=${this._triggerType}
@change=${i=>this._triggerType=i.target.value}
>
${Ds.map(i=>l`<option value=${i} ?selected=${i===this._triggerType}>${a(i,e)}</option>`)}
${Ws.map(i=>l`<option value=${i} ?selected=${i===this._triggerType}>${a(i,e)}</option>`)}
</select>
</div>
${t?this._renderCompoundEditor():l`
@@ -1569,7 +1625,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
</select>
</div>
`:_}
`}_patchCondition(e,t){this._compoundConditions=this._compoundConditions.map((i,n)=>n===e?{...i,...t}:i)}_addCondition(){this._compoundConditions=[...this._compoundConditions,Ms()]}_removeCondition(e){this._compoundConditions=this._compoundConditions.filter((t,i)=>i!==e)}_renderCompoundEditor(){let e=this._lang;return l`
`}_patchCondition(e,t){this._compoundConditions=this._compoundConditions.map((i,n)=>n===e?{...i,...t}:i)}_addCondition(){this._compoundConditions=[...this._compoundConditions,Vs()]}_removeCondition(e){this._compoundConditions=this._compoundConditions.filter((t,i)=>i!==e)}_renderCompoundEditor(){let e=this._lang;return l`
<div class="select-row">
<label>${a("compound_logic",e)}</label>
<select
@@ -1618,7 +1674,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.value=${e.type}
@change=${o=>this._patchCondition(t,{type:o.target.value})}
>
${Si.map(o=>l`<option value=${o} ?selected=${o===e.type}>${a(o,i)}</option>`)}
${Pi.map(o=>l`<option value=${o} ?selected=${o===e.type}>${a(o,i)}</option>`)}
</select>
</div>
${this._renderConditionTypeFields(e,t)}
@@ -1762,12 +1818,12 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
>
${["days","weeks","months","years"].map(t=>l`<option value=${t} ?selected=${t===this._intervalUnit}>${a("unit_"+t,e)}</option>`)}
</select>
</div>`}_toggleWeekday(e){this._weekdays=this._weekdays.includes(e)?this._weekdays.filter(t=>t!==e):[...this._weekdays,e]}_previewScheduleDict(){if(this._scheduleType==="one_time")return this._dueDate?{kind:"one_time",due_date:this._dueDate}:null;if(vt.includes(this._scheduleType))return{...this._buildSchedule(),...this._recurrenceExtras()};let e=parseInt(this._intervalDays,10);return this._scheduleType==="manual"||!e||e<=0?null:{kind:"interval",every:e,unit:this._intervalUnit,anchor:this._intervalAnchor,...this._recurrenceExtras()}}updated(e){super.updated?.(e),this._scheduleEntityPickerProbe();for(let t of e.keys())if(g._PREVIEW_RELEVANT.has(String(t))){this._schedulePreviewRefresh();return}}_scheduleEntityPickerProbe(){this._entityPickerFallback||this._pickerProbeTimer!==void 0||!this._open||this._scheduleType!=="sensor_based"||(this._pickerProbeTimer=setTimeout(()=>this._probeEntityPickers(),1500))}_probeEntityPickers(){if(this._pickerProbeTimer=void 0,this._entityPickerFallback||!this._open)return;let e=this.shadowRoot?.querySelector("ha-form.entity-picker-form"),t=(this.shadowRoot?.querySelector(".content")?.offsetHeight??0)>0;if(!e||!t){this._pickerProbeStrikes=0;return}let i=(d,u,m=0)=>{if(!(!d||m>10)){(d.tagName?.toLowerCase()??"")==="ha-entity-picker"&&u.push(d);for(let f of[d.shadowRoot,d])if(f)for(let v of Array.from(f.children??[]))i(v,u,m+1)}},n=[...this.shadowRoot?.querySelectorAll("ha-form.entity-picker-form")??[]],o=[];for(let d of n)i(d,o);let p=o.length===0||o.some(d=>d.offsetHeight===0);if(e.offsetHeight===0||p){if(this._pickerProbeStrikes+=1,this._pickerProbeStrikes>=2){this._entityPickerFallback=!0;return}this._pickerProbeTimer=setTimeout(()=>this._probeEntityPickers(),700)}else this._pickerProbeStrikes=0}_schedulePreviewRefresh(){this._previewTimer&&clearTimeout(this._previewTimer),this._previewTimer=setTimeout(()=>{this._fetchSchedulePreview()},300)}async _fetchSchedulePreview(){let e=this._open?this._previewScheduleDict():null;if(!e){this._schedulePreview=[],this._schedulePreviewEnded=!1;return}let t=++this._previewSeq;try{let i=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/schedule/preview",schedule:e,...this._lastPerformed?{last_performed:this._lastPerformed}:{}});if(t!==this._previewSeq)return;this._schedulePreview=i.occurrences||[],this._schedulePreviewEnded=!!i.series_ended}catch{}}_renderSchedulePreview(){if(this._schedulePreview.length===0)return _;let e=this._lang,t=this.scheduleTimeEnabled&&this._scheduleTime?` ${this._scheduleTime}`:"",i=this._schedulePreview.map((o,p)=>{let d=new Date(`${o}T12:00:00`).getDay();return`${Le(d===0?6:d-1,e,"short")} ${Y(o,e)}${p===0?t:""}`}).join(" \xB7 "),n=this._scheduleType==="time_based"&&this._intervalAnchor==="completion"?l`<div class="field-help">${a("schedule_preview_ontime",e)}</div>`:_;return l`
</div>`}_toggleWeekday(e){this._weekdays=this._weekdays.includes(e)?this._weekdays.filter(t=>t!==e):[...this._weekdays,e]}_previewScheduleDict(){if(this._scheduleType==="one_time")return this._dueDate?{kind:"one_time",due_date:this._dueDate}:null;if(yt.includes(this._scheduleType))return{...this._buildSchedule(),...this._recurrenceExtras()};let e=parseInt(this._intervalDays,10);return this._scheduleType==="manual"||!e||e<=0?null:{kind:"interval",every:e,unit:this._intervalUnit,anchor:this._intervalAnchor,...this._recurrenceExtras()}}updated(e){super.updated?.(e),this._scheduleEntityPickerProbe();for(let t of e.keys())if(g._PREVIEW_RELEVANT.has(String(t))){this._schedulePreviewRefresh();return}}_scheduleEntityPickerProbe(){this._entityPickerFallback||this._pickerProbeTimer!==void 0||!this._open||this._scheduleType!=="sensor_based"||(this._pickerProbeTimer=setTimeout(()=>this._probeEntityPickers(),1500))}_probeEntityPickers(){if(this._pickerProbeTimer=void 0,this._entityPickerFallback||!this._open)return;let e=this.shadowRoot?.querySelector("ha-form.entity-picker-form"),t=(this.shadowRoot?.querySelector(".content")?.offsetHeight??0)>0;if(!e||!t){this._pickerProbeStrikes=0;return}let i=(d,u,m=0)=>{if(!(!d||m>10)){(d.tagName?.toLowerCase()??"")==="ha-entity-picker"&&u.push(d);for(let f of[d.shadowRoot,d])if(f)for(let v of Array.from(f.children??[]))i(v,u,m+1)}},n=[...this.shadowRoot?.querySelectorAll("ha-form.entity-picker-form")??[]],o=[];for(let d of n)i(d,o);let p=o.length===0||o.some(d=>d.offsetHeight===0);if(e.offsetHeight===0||p){if(this._pickerProbeStrikes+=1,this._pickerProbeStrikes>=2){this._entityPickerFallback=!0;return}this._pickerProbeTimer=setTimeout(()=>this._probeEntityPickers(),700)}else this._pickerProbeStrikes=0}_schedulePreviewRefresh(){this._previewTimer&&clearTimeout(this._previewTimer),this._previewTimer=setTimeout(()=>{this._fetchSchedulePreview()},300)}async _fetchSchedulePreview(){let e=this._open?this._previewScheduleDict():null;if(!e){this._schedulePreview=[],this._schedulePreviewEnded=!1;return}let t=++this._previewSeq;try{let i=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/schedule/preview",schedule:e,...this._lastPerformed?{last_performed:this._lastPerformed}:{}});if(t!==this._previewSeq)return;this._schedulePreview=i.occurrences||[],this._schedulePreviewEnded=!!i.series_ended}catch{}}_renderSchedulePreview(){if(this._schedulePreview.length===0)return _;let e=this._lang,t=this.scheduleTimeEnabled&&this._scheduleTime?` ${this._scheduleTime}`:"",i=this._schedulePreview.map((o,p)=>{let d=new Date(`${o}T12:00:00`).getDay();return`${Le(d===0?6:d-1,e,"short")} ${Q(o,e)}${p===0?t:""}`}).join(" \xB7 "),n=this._scheduleType==="time_based"&&this._intervalAnchor==="completion"?l`<div class="field-help">${a("schedule_preview_ontime",e)}</div>`:_;return l`
<div class="trigger-live-hint schedule-preview">
${a("schedule_preview_title",e)}: ${i}${this._schedulePreviewEnded?l` <span class="field-help">${a("schedule_preview_ends",e)}</span>`:_}
${n}
</div>
`}_buildSchedule(){let e=i=>{let n=parseInt(this._calOffset,10)||0;return n&&(i.offset=Math.max(-15,Math.min(n,15))),i};if(this._scheduleType==="weekdays")return e({kind:"weekdays",weekdays:[...this._weekdays].sort((i,n)=>i-n)});if(this._scheduleType==="nth_weekday")return e({kind:"nth_weekday",nth:parseInt(this._nth,10),weekday:parseInt(this._nthWeekday,10)});let t={kind:"day_of_month",day:this._domLastDay?-1:parseInt(this._domDay,10)||1};return this._domBusiness&&(t.business=!0),e(t)}_recurrenceExtras(){let e={};if(this._seasonMonths.length&&(e.season_months=[...this._seasonMonths].sort((t,i)=>t-i)),this._endsMode==="count"){let t=parseInt(this._endsCount,10);t>=1&&(e.ends={count:t})}else this._endsMode==="until"&&this._endsUntil&&(e.ends={until:this._endsUntil});return e}_toggleSeasonMonth(e){this._seasonMonths=this._seasonMonths.includes(e)?this._seasonMonths.filter(t=>t!==e):[...this._seasonMonths,e]}_renderRecurrenceExtras(){let e=this._lang;if(!(this._scheduleType==="time_based"||vt.includes(this._scheduleType)))return _;let i=Bs(e);return l`
`}_buildSchedule(){let e=i=>{let n=parseInt(this._calOffset,10)||0;return n&&(i.offset=Math.max(-15,Math.min(n,15))),i};if(this._scheduleType==="weekdays")return e({kind:"weekdays",weekdays:[...this._weekdays].sort((i,n)=>i-n)});if(this._scheduleType==="nth_weekday")return e({kind:"nth_weekday",nth:parseInt(this._nth,10),weekday:parseInt(this._nthWeekday,10)});let t={kind:"day_of_month",day:this._domLastDay?-1:parseInt(this._domDay,10)||1};return this._domBusiness&&(t.business=!0),e(t)}_recurrenceExtras(){let e={};if(this._seasonMonths.length&&(e.season_months=[...this._seasonMonths].sort((t,i)=>t-i)),this._endsMode==="count"){let t=parseInt(this._endsCount,10);t>=1&&(e.ends={count:t})}else this._endsMode==="until"&&this._endsUntil&&(e.ends={until:this._endsUntil});return e}_toggleSeasonMonth(e){this._seasonMonths=this._seasonMonths.includes(e)?this._seasonMonths.filter(t=>t!==e):[...this._seasonMonths,e]}_renderRecurrenceExtras(){let e=this._lang;if(!(this._scheduleType==="time_based"||yt.includes(this._scheduleType)))return _;let i=Js(e);return l`
<label class="field-label">${a("season_window_label",e)}</label>
<div class="field-help">${a("season_window_hint",e)}</div>
<div class="weekday-chips season-chips">
@@ -1802,7 +1858,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.value=${this._endsUntil}
@input=${n=>this._endsUntil=n.target.value}
></ms-textfield>`:_}
`}_renderCalendarFields(){let e=this._lang,t=Us(e);if(this._scheduleType==="weekdays")return l`
`}_renderCalendarFields(){let e=this._lang,t=Qs(e);if(this._scheduleType==="weekdays")return l`
<label class="field-label">${a("recurrence_on_days",e)}</label>
<div class="weekday-chips">
${t.map((i,n)=>l`
@@ -1854,7 +1910,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
max="15"
.value=${this._calOffset}
@input=${t=>this._calOffset=t.target.value}
></ms-textfield>`}_renderTriggerLiveHint(){if(this._triggerType==="compound")return _;let e=this._triggerEntityId||this._triggerEntityIds[0];if(!e||!this.hass?.states)return _;let t=this.hass.states[e];if(!t)return _;let i=this._lang,n=t.attributes?.unit_of_measurement,o=typeof n=="string"&&n?` ${n}`:"",p=this._triggerAttribute?t.attributes?.[this._triggerAttribute]:t.state,d=typeof p=="number"?p:parseFloat(String(p)),u=p!=="unknown"&&p!=="unavailable"&&p!=null&&!isNaN(d),m=v=>Number.isInteger(v)?String(v):String(Math.round(v*10)/10),f=[];if(this._triggerType==="threshold"){let v=parseFloat(this._triggerAbove),b=parseFloat(this._triggerBelow);if(isNaN(v)&&isNaN(b))return _;u&&f.push(a("trigger_hint_now",i).replace("{value}",m(d)+o)),isNaN(v)||f.push(a("trigger_hint_above",i).replace("{target}",m(v)+o)),isNaN(b)||f.push(a("trigger_hint_below",i).replace("{target}",m(b)+o))}else if(this._triggerType==="counter"){let v=parseFloat(this._triggerTargetValue);if(isNaN(v))return _;this._triggerDeltaMode?this._taskId?f.push(a("trigger_hint_counter_delta_edit",i).replace("{target}",m(v)+o)):u?f.push(a("trigger_hint_counter_delta",i).replace("{value}",m(d)+o).replace("{due}",m(d+v)+o).replace("{target}",m(v)+o)):f.push(a("trigger_hint_counter_delta_edit",i).replace("{target}",m(v)+o)):(u&&f.push(a("trigger_hint_now",i).replace("{value}",m(d)+o)),f.push(a("trigger_hint_counter_abs",i).replace("{target}",m(v)+o)))}else if(this._triggerType==="runtime"){let v=parseFloat(this._triggerRuntimeHours);if(isNaN(v))return _;f.push(a("trigger_hint_runtime",i).replace("{hours}",m(v))),f.push(a("trigger_hint_state_now",i).replace("{value}",String(t.state)))}else if(this._triggerType==="state_change"){let v=parseInt(this._triggerTargetChanges,10)||1,b=this._triggerToState.trim();f.push((b?a("trigger_hint_state_change_to",i).replace("{state}",b):a("trigger_hint_state_change",i)).replace("{count}",String(v))),f.push(a("trigger_hint_state_now",i).replace("{value}",String(t.state)))}return f.length?l`<div class="trigger-live-hint">${f.join(" ")}</div>`:_}_renderTriggerTypeFields(){let e=this._lang;return this._triggerType==="threshold"?l`
></ms-textfield>`}_renderTriggerLiveHint(){if(this._triggerType==="compound")return _;let e=this._triggerEntityId||this._triggerEntityIds[0];if(!e||!this.hass?.states)return _;let t=this.hass.states[e];if(!t)return _;let i=this._lang,n=t.attributes?.unit_of_measurement,o=typeof n=="string"&&n?` ${n}`:"",p=this._triggerAttribute?t.attributes?.[this._triggerAttribute]:t.state,d=typeof p=="number"?p:parseFloat(String(p)),u=p!=="unknown"&&p!=="unavailable"&&p!=null&&!isNaN(d),m=v=>Number.isInteger(v)?String(v):String(Math.round(v*10)/10),f=[];if(this._triggerType==="threshold"){let v=parseFloat(this._triggerAbove),y=parseFloat(this._triggerBelow);if(isNaN(v)&&isNaN(y))return _;u&&f.push(a("trigger_hint_now",i).replace("{value}",m(d)+o)),isNaN(v)||f.push(a("trigger_hint_above",i).replace("{target}",m(v)+o)),isNaN(y)||f.push(a("trigger_hint_below",i).replace("{target}",m(y)+o))}else if(this._triggerType==="counter"){let v=parseFloat(this._triggerTargetValue);if(isNaN(v))return _;this._triggerDeltaMode?this._taskId?f.push(a("trigger_hint_counter_delta_edit",i).replace("{target}",m(v)+o)):u?f.push(a("trigger_hint_counter_delta",i).replace("{value}",m(d)+o).replace("{due}",m(d+v)+o).replace("{target}",m(v)+o)):f.push(a("trigger_hint_counter_delta_edit",i).replace("{target}",m(v)+o)):(u&&f.push(a("trigger_hint_now",i).replace("{value}",m(d)+o)),f.push(a("trigger_hint_counter_abs",i).replace("{target}",m(v)+o)))}else if(this._triggerType==="runtime"){let v=parseFloat(this._triggerRuntimeHours);if(isNaN(v))return _;f.push(a("trigger_hint_runtime",i).replace("{hours}",m(v))),f.push(a("trigger_hint_state_now",i).replace("{value}",String(t.state)))}else if(this._triggerType==="state_change"){let v=parseInt(this._triggerTargetChanges,10)||1,y=this._triggerToState.trim();f.push((y?a("trigger_hint_state_change_to",i).replace("{state}",y):a("trigger_hint_state_change",i)).replace("{count}",String(v))),f.push(a("trigger_hint_state_now",i).replace("{value}",String(t.state)))}return f.length?l`<div class="trigger-live-hint">${f.join(" ")}</div>`:_}_renderTriggerTypeFields(){let e=this._lang;return this._triggerType==="threshold"?l`
<ms-textfield
label="${a("trigger_above",e)}"
type="number"
@@ -1955,7 +2011,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
<div class="dialog-title">${t}</div>
<div class="content">
${this._error?l`<div class="error">${this._error}</div>`:_}
${this._objectChoices.length>0?l`
${this._taskId===null&&this._objectChoices.length>0?l`
<div class="select-row">
<label>${a("object",e)}</label>
<select
@@ -1978,7 +2034,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.value=${this._type}
@change=${i=>this._type=i.target.value}
>
${js.map(i=>l`<option value=${i} ?selected=${i===this._type}>${a(i,e)}</option>`)}
${zs.map(i=>l`<option value=${i} ?selected=${i===this._type}>${a(i,e)}</option>`)}
</select>
</div>
${this._type==="reading"?l`
@@ -2012,7 +2068,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.value=${this._priority}
@change=${i=>this._priority=i.target.value}
>
${Ns.map(i=>l`<option value=${i} ?selected=${i===this._priority}>${a("priority_"+i,e)}</option>`)}
${Us.map(i=>l`<option value=${i} ?selected=${i===this._priority}>${a("priority_"+i,e)}</option>`)}
</select>
</div>
<div class="field">
@@ -2031,7 +2087,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.value=${this._scheduleType}
@change=${i=>this._scheduleType=i.target.value}
>
${Hs.map(i=>l`<option value=${i} ?selected=${i===this._scheduleType}>${a(i,e)}</option>`)}
${Bs.map(i=>l`<option value=${i} ?selected=${i===this._scheduleType}>${a(i,e)}</option>`)}
</select>
</div>
${this._scheduleType==="time_based"?l`
@@ -2103,14 +2159,14 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
${this._renderPhasesEditor(e)}
<h3>${a("require_on_completion",e)}</h3>
<div class="required-completion">
${Ei.map(i=>l`
${ft.map(i=>l`
<label class="req-option">
<input
type="checkbox"
.checked=${this._requiredCompletion.includes(i)}
@change=${n=>this._toggleRequired(i,n.target.checked)}
/>
<span>${a(Ye[i],e)}</span>
<span>${a(Ne[i],e)}</span>
</label>
`)}
</div>
@@ -2169,7 +2225,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
<ha-form
class="entity-picker-form"
.hass=${this.hass}
.schema=${[{name:"environmental_entity",selector:{entity:{domain:wi,device_class:$i}}}]}
.schema=${[{name:"environmental_entity",selector:{entity:{domain:Ti,device_class:Ii}}}]}
.data=${{environmental_entity:this._environmentalEntity}}
.computeLabel=${()=>a("environmental_entity_optional",e)}
.computeHelper=${()=>a("environmental_entity_helper",e)}
@@ -2180,6 +2236,9 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
${this._renderAdaptiveSection(e)}
<ms-textfield
label="${a("notes_optional",e)}"
multiline
.rows=${3}
.helper=${a("notes_markdown_hint",e)}
.value=${this._notes}
@input=${i=>this._notes=i.target.value}
></ms-textfield>
@@ -2222,6 +2281,15 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
</button>
</div>
`}
<label class="req-option">
<input
type="checkbox"
.checked=${this._requireTagScan}
@change=${i=>this._requireTagScan=i.target.checked}
/>
<span>${a("require_tag_scan",e)}</span>
</label>
${this._requireTagScan?l`<div class="field-help">${a("require_tag_scan_help",e)}</div>`:_}
<label class="toggle-row">
<input
type="checkbox"
@@ -2391,6 +2459,15 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
min-height: 56px;
margin-top: 6px;
}
.phase-req-toggle {
margin-top: 6px;
font-size: 13px;
color: var(--secondary-text-color);
}
.phase-req-fields {
margin: 2px 0 0 22px;
font-size: 13px;
}
.phase-remove {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
@@ -2609,7 +2686,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
font-size: 14px;
cursor: pointer;
}
`,c([x({attribute:!1})],g.prototype,"hass",2),c([x({type:Boolean,attribute:"checklists-enabled"})],g.prototype,"checklistsEnabled",2),c([x({type:Boolean,attribute:"schedule-time-enabled"})],g.prototype,"scheduleTimeEnabled",2),c([x({type:Boolean,attribute:"completion-actions-enabled"})],g.prototype,"completionActionsEnabled",2),c([x({type:Number,attribute:"default-warning-days"})],g.prototype,"defaultWarningDays",2),c([h()],g.prototype,"parts",2),c([h()],g.prototype,"_foreignOwners",2),c([h()],g.prototype,"_open",2),c([h()],g.prototype,"_entityPickerFallback",2),c([h()],g.prototype,"_loading",2),c([h()],g.prototype,"_error",2),c([h()],g.prototype,"_entryId",2),c([h()],g.prototype,"_taskId",2),c([h()],g.prototype,"_objectChoices",2),c([h()],g.prototype,"_name",2),c([h()],g.prototype,"_type",2),c([h()],g.prototype,"_scheduleType",2),c([h()],g.prototype,"_intervalDays",2),c([h()],g.prototype,"_intervalUnit",2),c([h()],g.prototype,"_dueDate",2),c([h()],g.prototype,"_warningDays",2),c([h()],g.prototype,"_earliestCompletionDays",2),c([h()],g.prototype,"_intervalAnchor",2),c([h()],g.prototype,"_weekdays",2),c([h()],g.prototype,"_nth",2),c([h()],g.prototype,"_nthWeekday",2),c([h()],g.prototype,"_domDay",2),c([h()],g.prototype,"_domLastDay",2),c([h()],g.prototype,"_domBusiness",2),c([h()],g.prototype,"_calOffset",2),c([h()],g.prototype,"_seasonMonths",2),c([h()],g.prototype,"_endsMode",2),c([h()],g.prototype,"_endsCount",2),c([h()],g.prototype,"_endsUntil",2),c([h()],g.prototype,"_schedulePreview",2),c([h()],g.prototype,"_schedulePreviewEnded",2),c([h()],g.prototype,"_notes",2),c([h()],g.prototype,"_documentationUrl",2),c([h()],g.prototype,"_customIcon",2),c([h()],g.prototype,"_priority",2),c([h()],g.prototype,"_labels",2),c([h()],g.prototype,"_enabled",2),c([h()],g.prototype,"_triggerEntityId",2),c([h()],g.prototype,"_triggerEntityIds",2),c([h()],g.prototype,"_triggerEntityLogic",2),c([h()],g.prototype,"_triggerAttribute",2),c([h()],g.prototype,"_triggerType",2),c([h()],g.prototype,"_triggerAbove",2),c([h()],g.prototype,"_triggerBelow",2),c([h()],g.prototype,"_triggerEquals",2),c([h()],g.prototype,"_triggerNotEquals",2),c([h()],g.prototype,"_triggerForMinutes",2),c([h()],g.prototype,"_triggerCombinator",2),c([h()],g.prototype,"_triggerTargetValue",2),c([h()],g.prototype,"_triggerDeltaMode",2),c([h()],g.prototype,"_triggerBaselineValue",2),c([h()],g.prototype,"_liveBaselineValue",2),c([h()],g.prototype,"_autoCompleteOnRecovery",2),c([h()],g.prototype,"_triggerFromState",2),c([h()],g.prototype,"_triggerToState",2),c([h()],g.prototype,"_triggerTargetChanges",2),c([h()],g.prototype,"_triggerRuntimeHours",2),c([h()],g.prototype,"_triggerOnStates",2),c([h()],g.prototype,"_compoundLogic",2),c([h()],g.prototype,"_compoundConditions",2),c([h()],g.prototype,"_suggestedAttributes",2),c([h()],g.prototype,"_availableAttributes",2),c([h()],g.prototype,"_entityDomain",2),c([h()],g.prototype,"_lastPerformed",2),c([h()],g.prototype,"_nfcTagId",2),c([h()],g.prototype,"_readingUnit",2),c([h()],g.prototype,"_consumesParts",2),c([h()],g.prototype,"_partsLoadFailed",2),c([h()],g.prototype,"_availableTags",2),c([h()],g.prototype,"_responsibleUserId",2),c([h()],g.prototype,"_assigneePool",2),c([h()],g.prototype,"_rotationStrategy",2),c([h()],g.prototype,"_availableUsers",2),c([h()],g.prototype,"_checklistText",2),c([h()],g.prototype,"_phaseDefs",2),c([h()],g.prototype,"_phaseSeq",2),c([h()],g.prototype,"_requiredCompletion",2),c([h()],g.prototype,"_scheduleTime",2),c([h()],g.prototype,"_actionService",2),c([h()],g.prototype,"_actionTargetEntity",2),c([h()],g.prototype,"_actionData",2),c([h()],g.prototype,"_actionDataJsonFallback",2),c([h()],g.prototype,"_actionTesting",2),c([h()],g.prototype,"_actionTestResult",2),c([h()],g.prototype,"_actionTestError",2),c([h()],g.prototype,"_qcNotes",2),c([h()],g.prototype,"_qcCost",2),c([h()],g.prototype,"_qcDuration",2),c([h()],g.prototype,"_qcFeedback",2),c([h()],g.prototype,"_environmentalEntity",2),c([h()],g.prototype,"_environmentalAttribute",2),c([h()],g.prototype,"_adaptiveEnabled",2),c([h()],g.prototype,"_adaptiveAlpha",2),c([h()],g.prototype,"_adaptiveMin",2),c([h()],g.prototype,"_adaptiveMax",2),c([h()],g.prototype,"_adaptiveSeasonal",2),c([h()],g.prototype,"_adaptivePrediction",2),c([h()],g.prototype,"_conditionAttrOptions",2);yt=g;customElements.get("maintenance-task-dialog")||customElements.define("maintenance-task-dialog",yt)});var $,Ti=w(()=>{"use strict";L();U();R();le();Ge();ft();$=class extends A{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.restockUnitCost=null;this.currencySymbol="";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this.phaseLabel="";this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this Line truncated
`,c([x({attribute:!1})],g.prototype,"hass",2),c([x({type:Boolean,attribute:"checklists-enabled"})],g.prototype,"checklistsEnabled",2),c([x({type:Boolean,attribute:"schedule-time-enabled"})],g.prototype,"scheduleTimeEnabled",2),c([x({type:Boolean,attribute:"completion-actions-enabled"})],g.prototype,"completionActionsEnabled",2),c([x({type:Number,attribute:"default-warning-days"})],g.prototype,"defaultWarningDays",2),c([h()],g.prototype,"parts",2),c([h()],g.prototype,"_foreignOwners",2),c([h()],g.prototype,"_open",2),c([h()],g.prototype,"_entityPickerFallback",2),c([h()],g.prototype,"_loading",2),c([h()],g.prototype,"_error",2),c([h()],g.prototype,"_entryId",2),c([h()],g.prototype,"_taskId",2),c([h()],g.prototype,"_objectChoices",2),c([h()],g.prototype,"_name",2),c([h()],g.prototype,"_type",2),c([h()],g.prototype,"_scheduleType",2),c([h()],g.prototype,"_intervalDays",2),c([h()],g.prototype,"_intervalUnit",2),c([h()],g.prototype,"_dueDate",2),c([h()],g.prototype,"_warningDays",2),c([h()],g.prototype,"_earliestCompletionDays",2),c([h()],g.prototype,"_intervalAnchor",2),c([h()],g.prototype,"_weekdays",2),c([h()],g.prototype,"_nth",2),c([h()],g.prototype,"_nthWeekday",2),c([h()],g.prototype,"_domDay",2),c([h()],g.prototype,"_domLastDay",2),c([h()],g.prototype,"_domBusiness",2),c([h()],g.prototype,"_calOffset",2),c([h()],g.prototype,"_seasonMonths",2),c([h()],g.prototype,"_endsMode",2),c([h()],g.prototype,"_endsCount",2),c([h()],g.prototype,"_endsUntil",2),c([h()],g.prototype,"_schedulePreview",2),c([h()],g.prototype,"_schedulePreviewEnded",2),c([h()],g.prototype,"_notes",2),c([h()],g.prototype,"_documentationUrl",2),c([h()],g.prototype,"_customIcon",2),c([h()],g.prototype,"_priority",2),c([h()],g.prototype,"_labels",2),c([h()],g.prototype,"_enabled",2),c([h()],g.prototype,"_triggerEntityId",2),c([h()],g.prototype,"_triggerEntityIds",2),c([h()],g.prototype,"_triggerEntityLogic",2),c([h()],g.prototype,"_triggerAttribute",2),c([h()],g.prototype,"_triggerType",2),c([h()],g.prototype,"_triggerAbove",2),c([h()],g.prototype,"_triggerBelow",2),c([h()],g.prototype,"_triggerEquals",2),c([h()],g.prototype,"_triggerNotEquals",2),c([h()],g.prototype,"_triggerForMinutes",2),c([h()],g.prototype,"_triggerCombinator",2),c([h()],g.prototype,"_triggerTargetValue",2),c([h()],g.prototype,"_triggerDeltaMode",2),c([h()],g.prototype,"_triggerBaselineValue",2),c([h()],g.prototype,"_liveBaselineValue",2),c([h()],g.prototype,"_autoCompleteOnRecovery",2),c([h()],g.prototype,"_triggerFromState",2),c([h()],g.prototype,"_triggerToState",2),c([h()],g.prototype,"_triggerTargetChanges",2),c([h()],g.prototype,"_triggerRuntimeHours",2),c([h()],g.prototype,"_triggerOnStates",2),c([h()],g.prototype,"_compoundLogic",2),c([h()],g.prototype,"_compoundConditions",2),c([h()],g.prototype,"_suggestedAttributes",2),c([h()],g.prototype,"_availableAttributes",2),c([h()],g.prototype,"_entityDomain",2),c([h()],g.prototype,"_lastPerformed",2),c([h()],g.prototype,"_nfcTagId",2),c([h()],g.prototype,"_requireTagScan",2),c([h()],g.prototype,"_readingUnit",2),c([h()],g.prototype,"_consumesParts",2),c([h()],g.prototype,"_partsLoadFailed",2),c([h()],g.prototype,"_availableTags",2),c([h()],g.prototype,"_responsibleUserId",2),c([h()],g.prototype,"_assigneePool",2),c([h()],g.prototype,"_rotationStrategy",2),c([h()],g.prototype,"_availableUsers",2),c([h()],g.prototype,"_checklistText",2),c([h()],g.prototype,"_phaseDefs",2),c([h()],g.prototype,"_phaseSeq",2),c([h()],g.prototype,"_requiredCompletion",2),c([h()],g.prototype,"_scheduleTime",2),c([h()],g.prototype,"_actionService",2),c([h()],g.prototype,"_actionTargetEntity",2),c([h()],g.prototype,"_actionData",2),c([h()],g.prototype,"_actionDataJsonFallback",2),c([h()],g.prototype,"_actionTesting",2),c([h()],g.prototype,"_actionTestResult",2),c([h()],g.prototype,"_actionTestError",2),c([h()],g.prototype,"_qcNotes",2),c([h()],g.prototype,"_qcCost",2),c([h()],g.prototype,"_qcDuration",2),c([h()],g.prototype,"_qcFeedback",2),c([h()],g.prototype,"_environmentalEntity",2),c([h()],g.prototype,"_environmentalAttribute",2),c([h()],g.prototype,"_adaptiveEnabled",2),c([h()],g.prototype,"_adaptiveAlpha",2),c([h()],g.prototype,"_adaptiveMin",2),c([h()],g.prototype,"_adaptiveMax",2),c([h()],g.prototype,"_adaptiveSeasonal",2),c([h()],g.prototype,"_adaptivePrediction",2),c([h()],g.prototype,"_conditionAttrOptions",2);bt=g;customElements.get("maintenance-task-dialog")||customElements.define("maintenance-task-dialog",bt)});var $,qi=w(()=>{"use strict";P();W();j();de();Qe();vt();$=class extends A{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.restockUnitCost=null;this.currencySymbol="";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this.phaseLabel="";this.requireTagScan=!1;this.viaTagScan=!1;this._open=!1;this._notes="";this._cost="";this._duration="";t Line truncated
type="button"
class="cost-suggestion"
@click=${()=>this._cost=t.toFixed(2)}
@@ -2617,6 +2694,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${a("complete_title",e)}${this.taskName}</div>
${this.phaseLabel?l`<div class="phase-line">${a("phase_current",e)}: ${this.phaseLabel}</div>`:_}
${this.requireTagScan&&!this.viaTagScan?l`<div class="scan-required-note">${a("require_tag_scan_hint",e)}</div>`:_}
<div class="content">
${this._error?l`<div class="error">${this._error}</div>`:_}
${this.checklist.length>0?l`
@@ -2639,7 +2717,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
</label>`:_}
${this.parts.length?l`<div class="used-parts">
<span class="field-label">${a("complete_parts_used",e)}</span>
${this.parts.map(t=>{let i=B({part_id:t.id,entry_id:t.entry_id}),n=this._usedParts[i],o=n!==void 0,p=t.entry_id?{part_id:t.id,quantity:1,entry_id:t.entry_id}:{part_id:t.id,quantity:1};return l`<div class="used-part-row">
${this.parts.map(t=>{let i=V({part_id:t.id,entry_id:t.entry_id}),n=this._usedParts[i],o=n!==void 0,p=t.entry_id?{part_id:t.id,quantity:1,entry_id:t.entry_id}:{part_id:t.id,quantity:1};return l`<div class="used-part-row">
<label class="used-part-check">
<input type="checkbox" .checked=${o}
@change=${d=>{let u={...this._usedParts};d.target.checked?u[i]=u[i]||p:delete u[i],this._usedParts=u}} />
@@ -2736,13 +2814,13 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
<ha-button
@click=${this._complete}
.disabled=${this._loading||this._missingRequired.length>0}
title=${this._missingRequired.length?this._missingRequired.map(t=>a("err_required",e).replace("{field}",a(Ye[t]??t,e))).join(" \xB7 "):""}
title=${this._missingRequired.length?this._missingRequired.map(t=>a("err_required",e).replace("{field}",a(Ne[t]??t,e))).join(" \xB7 "):""}
>
${this._loading?a("completing",e):a("complete",e)}
</ha-button>
</div>
</ha-dialog>
`}};$.styles=[fi,S`
`}};$.styles=[xi,S`
.req-mark {
color: var(--error-color, #f44336);
margin-left: 2px;
@@ -2766,6 +2844,14 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
font-weight: 500;
padding-bottom: 12px;
}
.scan-required-note {
margin: -4px 0 12px;
padding: 8px 10px;
border-radius: 6px;
background: rgba(255, 152, 0, 0.12);
color: var(--primary-text-color);
font-size: 13px;
}
.phase-line {
margin-top: -8px;
padding-bottom: 12px;
@@ -2913,7 +2999,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
color: var(--text-primary-color, #fff);
border-color: var(--primary-color);
}
`],c([x({attribute:!1})],$.prototype,"hass",2),c([x()],$.prototype,"entryId",2),c([x()],$.prototype,"taskId",2),c([x()],$.prototype,"taskName",2),c([x()],$.prototype,"lang",2),c([x({type:Array})],$.prototype,"checklist",2),c([x({type:Boolean})],$.prototype,"adaptiveEnabled",2),c([x()],$.prototype,"taskType",2),c([x()],$.prototype,"readingUnit",2),c([x({attribute:!1})],$.prototype,"restockDefault",2),c([x({attribute:!1})],$.prototype,"restockUnitCost",2),c([x()],$.prototype,"currencySymbol",2),c([x({attribute:!1})],$.prototype,"parts",2),c([x({attribute:!1})],$.prototype,"consumesParts",2),c([x({type:Array})],$.prototype,"consumesInfo",2),c([x({type:Array})],$.prototype,"requiredFields",2),c([x()],$.prototype,"phaseLabel",2),c([h()],$.prototype,"_open",2),c([h()],$.prototype,"_notes",2),c([h()],$.prototype,"_cost",2),c([h()],$.prototype,"_duration",2),c([h()],$.prototype,"_loading",2),c([h()],$.prototype,"_error",2),c([h()],$.prototype,"_checklistState",2),c([h()],$.prototype,"_feedback",2),c([h()],$.prototype,"_photoDocId",2),c([h()],$.prototype,"_photoPreview",2),c([h()],$.prototype,"_photoUploading",2),c([h()],$.prototype,"_readingValue",2),c([h()],$.prototype,"_restockQty",2),c([h()],$.prototype,"_completedAt",2),c([h()],$.prototype,"_usedParts",2),c([x({attribute:!1})],$.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",$)});var z,Ci=w(()=>{"use strict";L();U();R();le();z=class extends A{constructor(){super(...arguments);this._open=!1;this._saving=!1;this._error="";this._draft=null;this._originalSnapshot=null;this._partOptions=null;this._partQty={};this._partQtyOriginal=""}get _lang(){return j(this.hass)}openEdit(e){this._draft={...e},this._originalSnapshot={...e},this._error="",this._open=!0,this._partOptions=null,this._partQty={},this._partQtyOriginal="",this._loadPartOptions()}async _loadPartOptions(){let e=this._draft;if(e)try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/parts/overview"}),i=[];for(let o of t.parts||[]){let p=o.entry_id===e.entry_id,d=o.consumers.some(u=>u.entry_id===e.entry_id&&u.task_id===e.task_id);!p&&!d||i.push({part_id:o.part_id,name:o.name,entry_id:o.entry_id,foreign:!p,object_name:o.object_name})}for(let o of e.used_parts||[]){let p=o.entry_id||e.entry_id;i.some(d=>d.part_id===o.part_id&&d.entry_id===p)||i.push({part_id:o.part_id,name:o.name||o.part_id,entry_id:p,foreign:p!==e.entry_id,object_name:null})}let n={};for(let o of e.used_parts||[])n[`${o.entry_id||e.entry_id}:${o.part_id}`]=o.quantity??1;this._partOptions=i,this._partQty=n,this._partQtyOriginal=this._partSelectionKey()}catch{this._partOptions=[]}}_partSelectionKey(){return JSON.stringify(Object.entries(this._partQty).filter(([,e])=>e>0).sort(([e],[t])=>e.localeCompare(t)))}close(){this._open=!1,this._error="",this._draft=null,this._originalSnapshot=null}_set(e,t){this._draft&&(this._draft={...this._draft,[e]:t})}async _save(){if(!(!this._draft||!this._originalSnapshot)){this._saving=!0,this._error="";try{let e={type:"maintenance_supporter/task/history/update",entry_id:this._draft.entry_id,task_id:this._draft.task_id,original_timestamp:this._originalSnapshot.original_timestamp};if(this._draft.timestamp!==this._originalSnapshot.timestamp&&(e.timestamp=this._draft.timestamp),this._draft.notes!==this._originalSnapshot.notes&&(e.notes=this._draft.notes),this._draft.cost!==this._originalSnapshot.cost&&(e.cost=this._draft.cost),this._draft.duration!==this._originalSnapshot.duration&&(e.duration=this._draft.duration),this._draft.completed_by!==this._originalSnapshot.completed_by&&(e.completed_by=this._draft.completed_by),this._partOptions!==null&&this._partSelectionKey()!==this._partQtyOriginal&&(e.used_parts=(this._partOptions||[]).filter(i=>(this._partQty[`${i.entry_id}:${i.part_id}`]||0)>0).map(i=>({part_id:i.part_id,quantity:this._partQty[`${i.entry_id}:${i.part_id}`],...i.foreign?{entry_id:i.entry_id}:{}}))),Object.keys(e).filter(i=>!["type","entry_id","task_id","original_timestamp"].includes(i)).length===0){this.close();return}await this.hass.connection.sendMessagePromise(e),this.dispatchEvent(new CustomEvent("history-entry-saved",{detail:{entry_id:this._draft.entry_id,task_id:this._draft.task_id,new_timestamp:this._draft.timestamp},bubbles:!0,composed:!0})),this.close()}catch(e){this._error=q(e,this._lang)}finally{this._saving=!1}}}render(){if(!this._open||!this._draft)return _;let e=this._lang,t=this._draft;return l`
`],c([x({attribute:!1})],$.prototype,"hass",2),c([x()],$.prototype,"entryId",2),c([x()],$.prototype,"taskId",2),c([x()],$.prototype,"taskName",2),c([x()],$.prototype,"lang",2),c([x({type:Array})],$.prototype,"checklist",2),c([x({type:Boolean})],$.prototype,"adaptiveEnabled",2),c([x()],$.prototype,"taskType",2),c([x()],$.prototype,"readingUnit",2),c([x({attribute:!1})],$.prototype,"restockDefault",2),c([x({attribute:!1})],$.prototype,"restockUnitCost",2),c([x()],$.prototype,"currencySymbol",2),c([x({attribute:!1})],$.prototype,"parts",2),c([x({attribute:!1})],$.prototype,"consumesParts",2),c([x({type:Array})],$.prototype,"consumesInfo",2),c([x({type:Array})],$.prototype,"requiredFields",2),c([x()],$.prototype,"phaseLabel",2),c([x({type:Boolean})],$.prototype,"requireTagScan",2),c([x({type:Boolean})],$.prototype,"viaTagScan",2),c([h()],$.prototype,"_open",2),c([h()],$.prototype,"_notes",2),c([h()],$.prototype,"_cost",2),c([h()],$.prototype,"_duration",2),c([h()],$.prototype,"_loading",2),c([h()],$.prototype,"_error",2),c([h()],$.prototype,"_checklistState",2),c([h()],$.prototype,"_feedback",2),c([h()],$.prototype,"_photoDocId",2),c([h()],$.prototype,"_photoPreview",2),c([h()],$.prototype,"_photoUploading",2),c([h()],$.prototype,"_readingValue",2),c([h()],$.prototype,"_restockQty",2),c([h()],$.prototype,"_completedAt",2),c([h()],$.prototype,"_usedParts",2),c([x({attribute:!1})],$.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",$)});var U,Ri=w(()=>{"use strict";P();W();j();de();U=class extends A{constructor(){super(...arguments);this._open=!1;this._saving=!1;this._error="";this._draft=null;this._originalSnapshot=null;this._partOptions=null;this._partQty={};this._partQtyOriginal=""}get _lang(){return D(this.hass)}openEdit(e){this._draft={...e},this._originalSnapshot={...e},this._error="",this._open=!0,this._partOptions=null,this._partQty={},this._partQtyOriginal="",this._loadPartOptions()}async _loadPartOptions(){let e=this._draft;if(e)try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/parts/overview"}),i=[];for(let o of t.parts||[]){let p=o.entry_id===e.entry_id,d=o.consumers.some(u=>u.entry_id===e.entry_id&&u.task_id===e.task_id);!p&&!d||i.push({part_id:o.part_id,name:o.name,entry_id:o.entry_id,foreign:!p,object_name:o.object_name})}for(let o of e.used_parts||[]){let p=o.entry_id||e.entry_id;i.some(d=>d.part_id===o.part_id&&d.entry_id===p)||i.push({part_id:o.part_id,name:o.name||o.part_id,entry_id:p,foreign:p!==e.entry_id,object_name:null})}let n={};for(let o of e.used_parts||[])n[`${o.entry_id||e.entry_id}:${o.part_id}`]=o.quantity??1;this._partOptions=i,this._partQty=n,this._partQtyOriginal=this._partSelectionKey()}catch{this._partOptions=[]}}_partSelectionKey(){return JSON.stringify(Object.entries(this._partQty).filter(([,e])=>e>0).sort(([e],[t])=>e.localeCompare(t)))}close(){this._open=!1,this._error="",this._draft=null,this._originalSnapshot=null}_set(e,t){this._draft&&(this._draft={...this._draft,[e]:t})}async _save(){if(!(!this._draft||!this._originalSnapshot)){this._saving=!0,this._error="";try{let e={type:"maintenance_supporter/task/history/update",entry_id:this._draft.entry_id,task_id:this._draft.task_id,original_timestamp:this._originalSnapshot.original_timestamp};if(this._draft.timestamp!==this._originalSnapshot.timestamp&&(e.timestamp=this._draft.timestamp),this._draft.notes!==this._originalSnapshot.notes&&(e.notes=this._draft.notes),this._draft.cost!==this._originalSnapshot.cost&&(e.cost=this._draft.cost),this._draft.duration!==this._originalSnapshot.duration&&(e.duration=this._draft.duration),this._draft.completed_by!==this._originalSnapshot.completed_by&&(e.completed_by=this._draft.completed_by),this._partOptions!==null&&this._partSelectionKey()!==this._partQtyOriginal&&(e.used_parts=(this._partOptions||[]).filter(i=>(this._partQty[`${i.entry_id}:${i.part_id}`]||0)>0).map(i=>({part_id:i.part_id,quantity:this._partQty[`${i.entry_id}:${i.part_id}`],...i.foreign?{entry_id:i.entry_id}:{}}))),Object.keys(e).filter(i=>!["type","entry_id","task_id","original_timestamp"].includes(i)).length===0){this.close();return}await this.hass.connection.sendMessagePromise(e),this.dispatchEvent(new CustomEvent("history-entry-saved",{detail:{entry_id:this._draft.entry_id,task_id:this._draft.task_id,new_timestamp:this._draft.timestamp},bubbles:!0,composed:!0})),this.close()}catch(e){this._error=N(e,this._lang)}finally{this._saving=!1}}}render(){if(!this._open||!this._draft)return _;let e=this._lang,t=this._draft;return l`
<div class="backdrop" @click=${this.close}></div>
<div class="dialog" role="dialog" aria-modal="true">
<h2>${a("history_edit_title",e)||"Edit history entry"}</h2>
@@ -2975,7 +3061,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
</button>
</div>
</div>
`}};z.styles=S`
`}};U.styles=S`
:host { display: contents; }
.backdrop {
position: fixed; inset: 0;
@@ -3051,7 +3137,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
.part-row-edit input[type="checkbox"] { width: auto; }
.part-label { flex: 1; color: var(--primary-text-color); }
.part-qty { width: 76px; }
`,c([x({attribute:!1})],z.prototype,"hass",2),c([h()],z.prototype,"_open",2),c([h()],z.prototype,"_saving",2),c([h()],z.prototype,"_error",2),c([h()],z.prototype,"_draft",2),c([h()],z.prototype,"_partOptions",2),c([h()],z.prototype,"_partQty",2);customElements.get("maintenance-history-edit-dialog")||customElements.define("maintenance-history-edit-dialog",z)});function Ii(r,s,e){let t=new Blob([r],{type:e}),i=URL.createObjectURL(t),n=document.createElement("a");n.href=i,n.download=s,n.target="_blank",n.rel="noopener",n.style.display="none",document.body.appendChild(n),n.dispatchEvent(new MouseEvent("click")),document.body.removeChild(n),setTimeout(()=>URL.revokeObjectURL(i),6e4)}var Pi=w(()=>{"use strict"});function me(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function Li(r){return!r.startsWith("data:image/svg+xml,")&&!r.startsWith("data:image/png;base64,")?"":me(r)}function Ws(r){return r.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var M,Ri=w(()=>{"use strict";L();U();R();Pi();M=class extends A{constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,t){this._entryId=e,this._taskId=null,this._objectName=t,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,t,i,n){this._entryId=e,this._taskId=t,this._objectName=i,this._taskName=n,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let t={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(t.task_id=this._taskId);let i=[this.hass.connection.sendMessagePromise({...t,action:"view"})];this._taskId&&i.push(this.hass.connection.sendMessagePromise({...t,action:"complete"}));let n=await Promise.all(i);if(e!==this._generateSeq)return;this._viewResult=n[0],n.length>1&&(this._completeResult=n[1])}catch(t){if(e!==this._generateSeq)return;let i=t?.code,n=t?.message;this._error=i==="no_url"||typeof n=="string"&&n.includes("No Home Assistant URL")?a("qr_error_no_url",this.lang):a("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,t=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,i=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),n=window.open("","_blank","width=600,height=500");if(!n)return;let o=this.lang||"en",p=me(t),d=me(i),u=!!this._completeResult,m=me(a("qr_action_view",o)),f=me(a("qr_action_complete",o));n.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
`,c([x({attribute:!1})],U.prototype,"hass",2),c([h()],U.prototype,"_open",2),c([h()],U.prototype,"_saving",2),c([h()],U.prototype,"_error",2),c([h()],U.prototype,"_draft",2),c([h()],U.prototype,"_partOptions",2),c([h()],U.prototype,"_partQty",2);customElements.get("maintenance-history-edit-dialog")||customElements.define("maintenance-history-edit-dialog",U)});function ji(r,s,e){let t=new Blob([r],{type:e}),i=URL.createObjectURL(t),n=document.createElement("a");n.href=i,n.download=s,n.target="_blank",n.rel="noopener",n.style.display="none",document.body.appendChild(n),n.dispatchEvent(new MouseEvent("click")),document.body.removeChild(n),setTimeout(()=>URL.revokeObjectURL(i),6e4)}var Ni=w(()=>{"use strict"});function fe(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function Hi(r){return!r.startsWith("data:image/svg+xml,")&&!r.startsWith("data:image/png;base64,")?"":fe(r)}function Zs(r){return r.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var F,Di=w(()=>{"use strict";P();W();j();Ni();F=class extends A{constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,t){this._entryId=e,this._taskId=null,this._objectName=t,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,t,i,n){this._entryId=e,this._taskId=t,this._objectName=i,this._taskName=n,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let t={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(t.task_id=this._taskId);let i=[this.hass.connection.sendMessagePromise({...t,action:"view"})];this._taskId&&i.push(this.hass.connection.sendMessagePromise({...t,action:"complete"}));let n=await Promise.all(i);if(e!==this._generateSeq)return;this._viewResult=n[0],n.length>1&&(this._completeResult=n[1])}catch(t){if(e!==this._generateSeq)return;let i=t?.code,n=t?.message;this._error=i==="no_url"||typeof n=="string"&&n.includes("No Home Assistant URL")?a("qr_error_no_url",this.lang):a("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,t=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,i=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),n=window.open("","_blank","width=600,height=500");if(!n)return;let o=this.lang||"en",p=fe(t),d=fe(i),u=!!this._completeResult,m=fe(a("qr_action_view",o)),f=fe(a("qr_action_complete",o));n.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="color-scheme" content="light">
<title>${p}</title>
<style>
@@ -3073,17 +3159,17 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"
${d?`<div class="sub">${d}</div>`:""}
<div class="qr-row">
<div class="qr-col">
<img src="${Li(this._viewResult.svg_data_uri)}" alt="QR Info" />
<img src="${Hi(this._viewResult.svg_data_uri)}" alt="QR Info" />
<div class="qr-label">${m}</div>
</div>
${u?`<div class="qr-col">
<img src="${Li(this._completeResult.svg_data_uri)}" alt="QR Complete" />
<img src="${Hi(this._completeResult.svg_data_uri)}" alt="QR Complete" />
<div class="qr-label">${f}</div>
</div>`:""}
</div>
<div class="url">${me(this._viewResult.url)}</div>
<div class="url">${fe(this._viewResult.url)}</div>
<script>setTimeout(()=>window.print(),300)<\/script>
</body></html>`),n.document.close()}_downloadSvg(e,t){let i=decodeURIComponent(e.svg_data_uri.replace("data:image/svg+xml,","")),n=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;Ii(i,`qr-${Ws(n)}-${t}.svg`,"image/svg+xml")}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._error="",this._loading=!1}render(){if(!this._open)return l``;let e=this.lang||this.hass?.language||"en",t=this._taskName?`${a("qr_code",e)}: ${this._objectName} \u2014 ${this._taskName}`:`${a("qr_code",e)}: ${this._objectName}`,i=!!this._viewResult;return l`
</body></html>`),n.document.close()}_downloadSvg(e,t){let i=decodeURIComponent(e.svg_data_uri.replace("data:image/svg+xml,","")),n=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;ji(i,`qr-${Zs(n)}-${t}.svg`,"image/svg+xml")}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._error="",this._loading=!1}render(){if(!this._open)return l``;let e=this.lang||this.hass?.language||"en",t=this._taskName?`${a("qr_code",e)}: ${this._objectName} \u2014 ${this._taskName}`:`${a("qr_code",e)}: ${this._objectName}`,i=!!this._viewResult;return l`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${t}</div>
<div class="content">
@@ -3144,7 +3230,7 @@ ${d?`<div class="sub">${d}</div>`:""}
</ha-button>
</div>
</ha-dialog>
`}};M.styles=S`
`}};F.styles=S`
.dialog-title {
font-size: 18px;
font-weight: 500;
@@ -3262,51 +3348,51 @@ ${d?`<div class="sub">${d}</div>`:""}
color: var(--text-primary-color, #fff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
`,c([x({attribute:!1})],M.prototype,"hass",2),c([x()],M.prototype,"lang",2),c([h()],M.prototype,"_open",2),c([h()],M.prototype,"_loading",2),c([h()],M.prototype,"_error",2),c([h()],M.prototype,"_viewResult",2),c([h()],M.prototype,"_completeResult",2),c([h()],M.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",M)});function Vs(r,s){if(s<=0)return 0;let e=typeof r=="number"&&Number.isFinite(r)?Math.trunc(r):0;return e<0?0:e%s}function Ks(r){return!!(r?.phases&&r.phase_sequence&&r.phase_sequence.length>0)}function bt(r){if(!r||!Ks(r))return null;let s=r.phase_sequence,e=Vs(r.phase_cursor,s.length),t=s[e],i=r.phases?.[t];return i?{id:t,name:i.name,index:e,count:s.length,notes:i.notes,checklist:i.checklist!==void 0?i.checklist:r.checklist??[],consumesParts:i.consumes_parts!==void 0?i.consumes_parts:r.consumes_parts??[],requiredFields:i.required_completion_fields!==void 0?i.required_completion_fields:r.required_completion_fields??[]}:null}function Qe(r){let s=bt(r);return s?`${s.index+1}/${s.count} \xB7 ${s.name}`:""}var qi=w(()=>{"use strict"});function ji(r,s){let e=r.interval_analysis,t=e?.weibull_beta,i=e?.weibull_eta;if(t==null||i==null||i<=0)return _;let n=r.interval_days??0,o=r.suggested_interval??n;return l`
`,c([x({attribute:!1})],F.prototype,"hass",2),c([x()],F.prototype,"lang",2),c([h()],F.prototype,"_open",2),c([h()],F.prototype,"_loading",2),c([h()],F.prototype,"_error",2),c([h()],F.prototype,"_viewResult",2),c([h()],F.prototype,"_completeResult",2),c([h()],F.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",F)});function Xs(r,s){if(s<=0)return 0;let e=typeof r=="number"&&Number.isFinite(r)?Math.trunc(r):0;return e<0?0:e%s}function er(r){return!!(r?.phases&&r.phase_sequence&&r.phase_sequence.length>0)}function xt(r){if(!r||!er(r))return null;let s=r.phase_sequence,e=Xs(r.phase_cursor,s.length),t=s[e],i=r.phases?.[t];return i?{id:t,name:i.name,index:e,count:s.length,notes:i.notes,checklist:i.checklist!==void 0?i.checklist:r.checklist??[],consumesParts:i.consumes_parts!==void 0?i.consumes_parts:r.consumes_parts??[],requiredFields:i.required_completion_fields!==void 0?i.required_completion_fields:r.required_completion_fields??[]}:null}function He(r){let s=xt(r);return s?`${s.index+1}/${s.count} \xB7 ${s.name}`:""}var wt=w(()=>{"use strict"});function Mi(r){let s=r.task??null,e=s?xt(s):null,t=e?e.consumesParts:s?.consumes_parts||[],i=!!s?.part_ref,n=r.objects.find(d=>d.entry_id===r.entryId)?.parts||[],o=i?n.find(d=>d.id===s.part_ref.part_id):void 0,p=r.checklistsEnabled??!0;return{entry_id:r.entryId,task_id:r.taskId,task_name:r.taskName,checklist:e?p?e.checklist:[]:r.checklist??[],adaptive_enabled:!!r.adaptiveEnabled,required_completion_fields:e?e.requiredFields:s?.required_completion_fields||[],task_type:s?.type||"",reading_unit:s?.reading_unit||"",parts:i?[]:Ai({consumes_parts:t},r.entryId,r.objects,r.lang),consumes_parts:i?[]:t,phase_label:e?He(s):"",require_tag_scan:!!s?.require_tag_scan,restock_default:i?o?.restock_quantity??1:null,restock_unit_cost:i?o?.cost??null:null,currency_symbol:r.currencySymbol??"",consumes_info:t.map(d=>Si(d,r.entryId,r.objects,r.lang)),checklist_prefill:s?.checklist_progress||{},via_tag_scan:!!r.viaTagScan}}function Oi(r,s,e){r.entryId=s.entry_id,r.taskId=s.task_id,r.taskName=s.task_name,r.lang=e,r.checklist=s.checklist??[],r.adaptiveEnabled=!!s.adaptive_enabled,r.requiredFields=s.required_completion_fields??[],r.taskType=s.task_type??"",r.readingUnit=s.reading_unit??"",r.parts=s.parts??[],r.consumesParts=s.consumes_parts??[],r.phaseLabel=s.phase_label??"",r.requireTagScan=!!s.require_tag_scan,r.restockDefault=s.restock_default??null,r.restockUnitCost=s.restock_unit_cost??null,r.currencySymbol=s.currency_symbol??"",r.consumesInfo=s.consumes_info??[],r.checklistPrefill=s.checklist_prefill??{},r.viaTagScan=!!s.via_tag_scan,r.open({viaTagScan:!!s.via_tag_scan})}var $t=w(()=>{"use strict";Qe();wt()});function Fi(r,s){let e=r.interval_analysis,t=e?.weibull_beta,i=e?.weibull_eta;if(t==null||i==null||i<=0)return _;let n=r.interval_days??0,o=r.suggested_interval??n;return l`
<div class="weibull-section">
<div class="weibull-title">
<ha-svg-icon aria-hidden="true" path="M3,14L3.5,14.07L8.07,9.5C7.89,8.85 8.06,8.11 8.59,7.59C9.37,6.8 10.63,6.8 11.41,7.59C11.94,8.11 12.11,8.85 11.93,9.5L14.5,12.07L15,12C15.18,12 15.35,12 15.5,12.07L19.07,8.5C19,8.35 19,8.18 19,8A2,2 0 0,1 21,6A2,2 0 0,1 23,8A2,2 0 0,1 21,10C20.82,10 20.65,10 20.5,9.93L16.93,13.5C17,13.65 17,13.82 17,14A2,2 0 0,1 15,16A2,2 0 0,1 13,14L13.07,13.5L10.5,10.93C10.18,11 9.82,11 9.5,10.93L4.93,15.5L5,16A2,2 0 0,1 3,18A2,2 0 0,1 1,16A2,2 0 0,1 3,14Z"></ha-svg-icon>
${a("weibull_reliability_curve",s)}
${Gs(t,s)}
${tr(t,s)}
</div>
${Ys(t,i,n,o,s)}
${Qs(e,s)}
${e?.confidence_interval_low!=null?Js(e,r,s):_}
${ir(t,i,n,o,s)}
${sr(e,s)}
${e?.confidence_interval_low!=null?rr(e,r,s):_}
</div>
`}function Gs(r,s){let e,t,i;return r<.8?(e="early_failures",t="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z",i="beta_early_failures"):r<=1.2?(e="random_failures",t="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,17H11V15H13V17M13,13H11V7H13V13Z",i="beta_random_failures"):r<=3.5?(e="wear_out",t="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12H12V6Z",i="beta_wear_out"):(e="highly_predictable",t="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z",i="beta_highly_predictable"),l`
`}function tr(r,s){let e,t,i;return r<.8?(e="early_failures",t="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z",i="beta_early_failures"):r<=1.2?(e="random_failures",t="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,17H11V15H13V17M13,13H11V7H13V13Z",i="beta_random_failures"):r<=3.5?(e="wear_out",t="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12H12V6Z",i="beta_wear_out"):(e="highly_predictable",t="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z",i="beta_highly_predictable"),l`
<span class="beta-badge ${e}">
<ha-svg-icon path="${t}"></ha-svg-icon>
${a(i,s)} (\u03B2=${r.toFixed(2)})
</span>
`}function Ys(r,s,e,t,i){let b=Math.max(e,t,s,1)*1.3,k=50,E=[];for(let I=0;I<=k;I++){let P=I/k*b,te=1-Math.exp(-Math.pow(P/s,r)),ve=32+P/b*260,ye=136-te*128;E.push([ve,ye])}let O=E.map(([I,P])=>`${I.toFixed(1)},${P.toFixed(1)}`).join(" "),K="M32,136 "+E.map(([I,P])=>`L${I.toFixed(1)},${P.toFixed(1)}`).join(" ")+` L${E[k][0].toFixed(1)},136 Z`,D=32+e/b*260,ce=1-Math.exp(-Math.pow(e/s,r)),de=136-ce*128,y=((1-ce)*100).toFixed(0),F=32+t/b*260,pe=[0,.25,.5,.75,1];return l`
`}function ir(r,s,e,t,i){let y=Math.max(e,t,s,1)*1.3,k=50,E=[];for(let C=0;C<=k;C++){let R=C/k*y,se=1-Math.exp(-Math.pow(R/s,r)),ye=32+R/y*260,be=136-se*128;E.push([ye,be])}let H=E.map(([C,R])=>`${C.toFixed(1)},${R.toFixed(1)}`).join(" "),M="M32,136 "+E.map(([C,R])=>`L${C.toFixed(1)},${R.toFixed(1)}`).join(" ")+` L${E[k][0].toFixed(1)},136 Z`,q=32+e/y*260,z=1-Math.exp(-Math.pow(e/s,r)),te=136-z*128,b=((1-z)*100).toFixed(0),B=32+t/y*260,pe=[0,.25,.5,.75,1];return l`
<div class="weibull-chart">
<svg viewBox="0 0 ${300} ${160}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${a("chart_weibull",i)}">
${pe.map(I=>{let P=136-I*128;return _e`
<line x1="${32}" y1="${P.toFixed(1)}" x2="${292}" y2="${P.toFixed(1)}"
stroke="var(--divider-color)" stroke-width="0.5" stroke-dasharray="${I===.5?"4,3":_}" />
<text x="${28}" y="${(P+3).toFixed(1)}" fill="var(--secondary-text-color)"
font-size="8" text-anchor="end">${(I*100).toFixed(0)}%</text>
${pe.map(C=>{let R=136-C*128;return _e`
<line x1="${32}" y1="${R.toFixed(1)}" x2="${292}" y2="${R.toFixed(1)}"
stroke="var(--divider-color)" stroke-width="0.5" stroke-dasharray="${C===.5?"4,3":_}" />
<text x="${28}" y="${(R+3).toFixed(1)}" fill="var(--secondary-text-color)"
font-size="8" text-anchor="end">${(C*100).toFixed(0)}%</text>
`})}
<text x="${32}" y="${156}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">0</text>
<text x="${324/2}" y="${156}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">${Math.round(b/2)}</text>
<text x="${292}" y="${156}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">${Math.round(b)}</text>
<text x="${324/2}" y="${156}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">${Math.round(y/2)}</text>
<text x="${292}" y="${156}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">${Math.round(y)}</text>
<path d="${K}" fill="var(--primary-color, #03a9f4)" opacity="0.08" />
<polyline points="${O}" fill="none"
<path d="${M}" fill="var(--primary-color, #03a9f4)" opacity="0.08" />
<polyline points="${H}" fill="none"
stroke="var(--primary-color, #03a9f4)" stroke-width="2" />
${e>0?_e`
<line x1="${D.toFixed(1)}" y1="${8}" x2="${D.toFixed(1)}" y2="${136 .toFixed(1)}"
<line x1="${q.toFixed(1)}" y1="${8}" x2="${q.toFixed(1)}" y2="${136 .toFixed(1)}"
stroke="var(--primary-color, #03a9f4)" stroke-width="1.5" stroke-dasharray="4,3" />
<circle cx="${D.toFixed(1)}" cy="${de.toFixed(1)}" r="3"
<circle cx="${q.toFixed(1)}" cy="${te.toFixed(1)}" r="3"
fill="var(--primary-color, #03a9f4)" />
<text x="${(D+4).toFixed(1)}" y="${(de-6).toFixed(1)}" fill="var(--primary-color, #03a9f4)"
font-size="9" font-weight="600">R=${y}%</text>
<text x="${(q+4).toFixed(1)}" y="${(te-6).toFixed(1)}" fill="var(--primary-color, #03a9f4)"
font-size="9" font-weight="600">R=${b}%</text>
`:_}
${t>0&&t!==e?_e`
<line x1="${F.toFixed(1)}" y1="${8}" x2="${F.toFixed(1)}" y2="${136 .toFixed(1)}"
<line x1="${B.toFixed(1)}" y1="${8}" x2="${B.toFixed(1)}" y2="${136 .toFixed(1)}"
stroke="var(--success-color, #4caf50)" stroke-width="1.5" stroke-dasharray="4,3" />
`:_}
@@ -3321,7 +3407,7 @@ ${d?`<div class="sub">${d}</div>`:""}
${e>0?l`<span class="legend-item"><span class="legend-swatch" style="background:var(--primary-color, #03a9f4); opacity:0.5"></span> ${a("current_interval_marker",i)}</span>`:_}
${t>0&&t!==e?l`<span class="legend-item"><span class="legend-swatch" style="background:var(--success-color, #4caf50)"></span> ${a("recommended_marker",i)}</span>`:_}
</div>
`}function Qs(r,s){return l`
`}function sr(r,s){return l`
<div class="weibull-info-row">
<div class="weibull-info-item">
<span>${a("characteristic_life",s)}</span>
@@ -3334,14 +3420,14 @@ ${d?`<div class="sub">${d}</div>`:""}
</div>
`:_}
</div>
`}function Js(r,s,e){let t=r.confidence_interval_low,i=r.confidence_interval_high,n=s.suggested_interval??s.interval_days??0,o=s.interval_days??0,p=Math.max(0,t-5),u=i+5-p,m=(t-p)/u*100,f=(i-t)/u*100,v=(n-p)/u*100,b=o>0?(o-p)/u*100:-1;return l`
`}function rr(r,s,e){let t=r.confidence_interval_low,i=r.confidence_interval_high,n=s.suggested_interval??s.interval_days??0,o=s.interval_days??0,p=Math.max(0,t-5),u=i+5-p,m=(t-p)/u*100,f=(i-t)/u*100,v=(n-p)/u*100,y=o>0?(o-p)/u*100:-1;return l`
<div class="confidence-range">
<div class="confidence-range-title">
${a("confidence_interval",e)}: ${n} ${a("days",e)} (${t}\u2013${i})
</div>
<div class="confidence-bar">
<div class="confidence-fill" style="left:${m.toFixed(1)}%;width:${f.toFixed(1)}%"></div>
${b>=0?l`<div class="confidence-marker current" style="left:${b.toFixed(1)}%"></div>`:_}
${y>=0?l`<div class="confidence-marker current" style="left:${y.toFixed(1)}%"></div>`:_}
<div class="confidence-marker recommended" style="left:${v.toFixed(1)}%"></div>
</div>
<div class="confidence-labels">
@@ -3349,7 +3435,7 @@ ${d?`<div class="sub">${d}</div>`:""}
<span class="confidence-text high">${a("confidence_aggressive",e)} (${i}${a("days",e).charAt(0)})</span>
</div>
</div>
`}var Ni=w(()=>{"use strict";L();R()});function Hi(r,s,e){let t=r.degradation_trend!=null&&r.degradation_trend!=="insufficient_data",i=r.days_until_threshold!=null,n=r.environmental_factor!=null&&r.environmental_factor!==1;if(!t&&!i&&!n)return _;let o=r.degradation_trend==="rising"?"M16,6L18.29,8.29L13.41,13.17L9.41,9.17L2,16.59L3.41,18L9.41,12L13.41,16L19.71,9.71L22,12V6H16Z":r.degradation_trend==="falling"?"M16,18L18.29,15.71L13.41,10.83L9.41,14.83L2,7.41L3.41,6L9.41,12L13.41,8L19.71,14.29L22,12V18H16Z":"M22,12L18,8V11H3V13H18V16L22,12Z";return l`
`}var zi=w(()=>{"use strict";P();j()});function Ui(r,s,e){let t=r.degradation_trend!=null&&r.degradation_trend!=="insufficient_data",i=r.days_until_threshold!=null,n=r.environmental_factor!=null&&r.environmental_factor!==1;if(!t&&!i&&!n)return _;let o=r.degradation_trend==="rising"?"M16,6L18.29,8.29L13.41,13.17L9.41,9.17L2,16.59L3.41,18L9.41,12L13.41,16L19.71,9.71L22,12V6H16Z":r.degradation_trend==="falling"?"M16,18L18.29,15.71L13.41,10.83L9.41,14.83L2,7.41L3.41,6L9.41,12L13.41,8L19.71,14.29L22,12V18H16Z":"M22,12L18,8V11H3V13H18V16L22,12Z";return l`
<div class="prediction-section">
${r.sensor_prediction_urgency?l`
<div class="prediction-urgency-banner">
@@ -3375,8 +3461,9 @@ ${d?`<div class="sub">${d}</div>`:""}
<ha-svg-icon path="M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22A9,9 0 0,0 21,13A9,9 0 0,0 12,4M12.5,8H11V14L15.75,16.85L16.5,15.62L12.5,13.25V8M7.88,3.39L6.6,1.86L2,5.71L3.29,7.24L7.88,3.39M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25L22,5.72Z"></ha-svg-icon>
<span class="prediction-label">${a("days_until_threshold",s)}</span>
<span class="prediction-value prediction-days${r.days_until_threshold===0?" exceeded":r.sensor_prediction_urgency?" urgent":""}">${r.days_until_threshold===0?a("threshold_exceeded",s):"~"+Math.round(r.days_until_threshold)+" "+a("days",s)}</span>
${r.threshold_prediction_date?l`<span class="prediction-date">${Y(r.threshold_prediction_date,s)}</span>`:_}
${r.threshold_prediction_date?l`<span class="prediction-date">${Q(r.threshold_prediction_date,s)}</span>`:_}
${r.threshold_prediction_confidence?l`<span class="confidence-dot ${r.threshold_prediction_confidence}"></span>`:_}
${(r.prediction_cycles??0)>0?l`<span class="prediction-cycles">${a("prediction_cycles",s)}: ${r.prediction_cycles}</span>`:_}
</div>
`:_}
${n&&e.environmental?l`
@@ -3384,12 +3471,12 @@ ${d?`<div class="sub">${d}</div>`:""}
<ha-svg-icon path="M15,13V5A3,3 0 0,0 12,2A3,3 0 0,0 9,5V13A5,5 0 0,0 7,17A5,5 0 0,0 12,22A5,5 0 0,0 17,17A5,5 0 0,0 15,13M12,4A1,1 0 0,1 13,5V8H11V5A1,1 0 0,1 12,4Z"></ha-svg-icon>
<span class="prediction-label">${a("environmental_adjustment",s)}</span>
<span class="prediction-value">${r.environmental_factor.toFixed(2)}x</span>
${r.environmental_entity?l`<span class="prediction-entity entity-link" @click=${p=>mi(p,r.environmental_entity)}>${r.environmental_entity}</span>`:_}
${r.environmental_entity?l`<span class="prediction-entity entity-link" @click=${p=>bi(p,r.environmental_entity)}>${r.environmental_entity}</span>`:_}
</div>
`:_}
</div>
</div>
`}var Di=w(()=>{"use strict";L();R()});function Mi(r,s,e,t){let i=Math.max(r||1,s);return l`
`}var Bi=w(()=>{"use strict";P();j()});function Wi(r,s,e,t){let i=Math.max(r||1,s);return l`
<div class="interval-comparison">
<div class="interval-bar">
<div class="interval-label">
@@ -3407,7 +3494,7 @@ ${d?`<div class="sub">${d}</div>`:""}
style="width: ${Math.min(s/i*100,100)}%"></div>
</div>
</div>
`}var Oi=w(()=>{"use strict";L();R()});function Fi(r,s,e){if(!e.seasonal||!r.seasonal_factor||r.seasonal_factor===1)return _;let t=zi.map(p=>a(p,s)),i=new Date().getMonth(),n=r.seasonal_factors||r.interval_analysis?.seasonal_factors||null,o=n&&n.length===12?n:t.map((p,d)=>{let u=r.seasonal_factor||1,m=Math.sin((d-6)*Math.PI/6)*.3;return Math.max(.7,Math.min(1.3,u+m))});return l`
`}var Vi=w(()=>{"use strict";P();j()});function Gi(r,s,e){if(!e.seasonal||!r.seasonal_factor||r.seasonal_factor===1)return _;let t=Ki.map(p=>a(p,s)),i=new Date().getMonth(),n=r.seasonal_factors||r.interval_analysis?.seasonal_factors||null,o=n&&n.length===12?n:t.map((p,d)=>{let u=r.seasonal_factor||1,m=Math.sin((d-6)*Math.PI/6)*.3;return Math.max(.7,Math.min(1.3,u+m))});return l`
<div class="seasonal-card-compact">
<h4>${a("seasonal_awareness",s)}</h4>
<div class="seasonal-mini-chart">
@@ -3424,7 +3511,7 @@ ${d?`<div class="sub">${d}</div>`:""}
<span class="legend-item"><span class="dot high"></span> ${a("longer",s)||"L\xE4nger"}</span>
</div>
</div>
`}function Ui(r,s){return Zs(r,s)}function Zs(r,s){let e=r.seasonal_factors??r.interval_analysis?.seasonal_factors;if(!e||e.length!==12)return _;let t=r.interval_analysis?.seasonal_reason,i=new Date().getMonth(),n=300,o=100,p=8,u=o-p-4,m=Math.max(...e,1.5),f=n/12,v=f*.65,b=p+u-1/m*u;return l`
`}function Yi(r,s){return ar(r,s)}function ar(r,s){let e=r.seasonal_factors??r.interval_analysis?.seasonal_factors;if(!e||e.length!==12)return _;let t=r.interval_analysis?.seasonal_reason,i=new Date().getMonth(),n=300,o=100,p=8,u=o-p-4,m=Math.max(...e,1.5),f=n/12,v=f*.65,y=p+u-1/m*u;return l`
<div class="seasonal-chart">
<div class="seasonal-chart-title">
<ha-svg-icon aria-hidden="true" path="M17.75 4.09L15.22 6.03L16.13 9.09L13.5 7.28L10.87 9.09L11.78 6.03L9.25 4.09L12.44 4L13.5 1L14.56 4L17.75 4.09M21.25 11L19.61 12.25L20.2 14.23L18.5 13.06L16.8 14.23L17.39 12.25L15.75 11L17.81 10.95L18.5 9L19.19 10.95L21.25 11M18.97 15.95C19.8 15.87 20.69 17.05 20.16 17.8C19.84 18.25 19.5 18.67 19.08 19.07C15.17 23 8.84 23 4.94 19.07C1.03 15.17 1.03 8.83 4.94 4.93C5.34 4.53 5.76 4.17 6.21 3.85C6.96 3.32 8.14 4.21 8.06 5.04C7.79 7.9 8.75 10.87 10.95 13.06C13.14 15.26 16.1 16.22 18.97 15.95Z"></ha-svg-icon>
@@ -3432,22 +3519,22 @@ ${d?`<div class="sub">${d}</div>`:""}
${t?l`<span class="source-tag">${t==="learned"?a("seasonal_learned",s):a("seasonal_manual",s)}</span>`:_}
</div>
<svg viewBox="0 0 ${n} ${o}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${a("chart_seasonal",s)}">
<line x1="0" y1="${b.toFixed(1)}" x2="${n}" y2="${b.toFixed(1)}"
<line x1="0" y1="${y.toFixed(1)}" x2="${n}" y2="${y.toFixed(1)}"
stroke="var(--divider-color)" stroke-width="1" stroke-dasharray="4,3" />
${e.map((k,E)=>{let O=k/m*u,K=E*f+(f-v)/2,D=p+u-O,ce=E===i,de=k<1?"var(--success-color, #4caf50)":k>1?"var(--warning-color, #ff9800)":"var(--secondary-text-color)";return _e`
<rect x="${K.toFixed(1)}" y="${D.toFixed(1)}"
width="${v.toFixed(1)}" height="${O.toFixed(1)}"
fill="${de}" opacity="${ce?1:.5}" rx="2" />
${e.map((k,E)=>{let H=k/m*u,M=E*f+(f-v)/2,q=p+u-H,z=E===i,te=k<1?"var(--success-color, #4caf50)":k>1?"var(--warning-color, #ff9800)":"var(--secondary-text-color)";return _e`
<rect x="${M.toFixed(1)}" y="${q.toFixed(1)}"
width="${v.toFixed(1)}" height="${H.toFixed(1)}"
fill="${te}" opacity="${z?1:.5}" rx="2" />
`})}
</svg>
<div class="seasonal-labels">
${zi.map((k,E)=>l`<span class="seasonal-label ${E===i?"active-month":""}">${a(k,s)}</span>`)}
${Ki.map((k,E)=>l`<span class="seasonal-label ${E===i?"active-month":""}">${a(k,s)}</span>`)}
</div>
</div>
`}var zi,Bi=w(()=>{"use strict";L();R();zi=["month_jan","month_feb","month_mar","month_apr","month_may","month_jun","month_jul","month_aug","month_sep","month_oct","month_nov","month_dec"]});var C,Wi=w(()=>{"use strict";L();U();R();le();ht();Ge();qi();Ni();Di();Oi();Bi();C=class extends A{constructor(){super(...arguments);this._open=!1;this._entryId=null;this._taskId=null;this._task=null;this._objectName="";this._busy=!1;this._error="";this._showSkip=!1;this._showReset=!1;this._showDetails=!1;this._showAdaptive=!1;this._skipReason="";this._resetDate="";this._features={adaptive:!1,predictions:!1,seasonal:!1,environmental:!1,budget:!1,groups:!1,checklists:!1,schedule_time:!1,completion_actions:!1};this._toast="";this._featuresLoaded=!1}get _lang(){return j(this.hass)}async openFor(e,t){this._entryId=e,this._taskId=t,this._error="",this._showSkip=!1,this._showReset=!1,this._showAdaptive=!1,this._skipReason="",this._resetDate=ne(new Date),this._open=!0,await Promise.all([this._loadTask(),this._loadFeatures()])}async _loadFeatures(){if(!this._featuresLoaded)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/settings"});e?.features&&(this._features={...this._features,...e.features}),this._featuresLoaded=!0}catch{}}close(){this._open=!1,this._task=null,this._error=""}async _loadTask(){if(!(!this._entryId||!this._taskId))try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId});this._objectName=e.object?.name||"";let t=(e.tasks||[]).find(i=>i.id===this._taskId);this._task=t??null}catch(e){this._error=q(e,this._lang)}}async _runWs(e){this._busy=!0,this._error="";try{return await this.hass.connection.sendMessagePromise(e),this._busy=!1,!0}catch(t){return this._error=q(t,this._lang),this._busy=!1,!1}}_notifyChanged(e){this.dispatchEvent(new CustomEvent("task-action-fired",{detail:{entry_id:this._entryId,task_id:this._taskId,action:e},bubbles:!0,composed:!0}))}_onComplete(){!this._entryId||!this._taskId||!this._task||Promise.resolve().then(()=>(W(),J)).then(async({openCompleteDialog:e})=>{let t=this._task,i=!!t.part_ref,n=bt(t),o=n?n.consumesParts:t.consumes_parts||[],p=[];if(!i)try{let u=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects",compact:!0});p=xi({consumes_parts:o},this._entryId,u.objects||[],this._lang)}catch{}e({entry_id:this._entryId,task_id:this._taskId,task_name:t.name,checklist:n?n.checklist:t.checklist||[],adaptive_enabled:!!t.adaptive_config?.enabled,required_completion_fields:n?n.requiredFields:t.required_completion_fields||[],task_type:t.type||"",reading_unit:t.reading_unit||"",parts:p,consumes_parts:i?[]:o,phase_label:Qe(t)})&&(this._notifyChanged("complete"),this.close())})}async _onSkipConfirm(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/skip",entry_id:this._entryId,task_id:this._taskId,reason:this._skipReason.trim()||null})&&(this._notifyChanged("skip"),this.close())}async _onResetConfirm(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/reset",entry_id:this._entryId,task_id:this._taskId,date:this._resetDate||void 0})&&(this._notifyChanged("reset"),this.close())}_onEdit(){!this._entryId||!this._taskId||Promise.resolve().then(()=>(W(),J)).then(({openEditTaskDialog:e})=>{e(this._entryId,this._taskId),this.close()})}_onQr(){!this._entryId||!this._taskId||!this._task||Promise.resolve().then(()=>(W(),J)).then(({openQrDialog:e})=>{e({entry_id:this._entryId,task_id:this._taskId,task_name:this._task.name,object_name:this._objectName}),this.close()})}async _onDelete(){if(!this._entryId||!this._taskId)return;let e=a("delete_task_confirm",this._lang)||`Delete "${this._task?.name}"?`;if(!window.confirm(e))return;await this._runWs({type:"maintenance_supporter/task/delete",entry_id:this._entryId,task_id:this._taskId})&&(this._notifyChanged("delete"),this.close())}async _onArchive(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/archive",entry_id:this._entryId,task_id:this._taskId})&&(this._notifyChanged("archive"),this.close())}async _onUnarchive(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/unarchive",entry_id:this._entryId,task_id:this._taskId})&&(this._notifyChanged("unarchive"),this.close())}_onOpenInPanel(){if(!this._entryId||!this._taskId)return;let e=`/maintenance-supporter?entry_id=${encodeURIComponent(this._entryId)}&task_id=${encodeURIComponent(this._taskId)}`;history.pushState(null,"",e),window.dispatchEvent(new CustomEvent("location-changed")),this.close()}async _applySuggestion(){if(!this._entryId||!this._taskId||!this._task?.suggested_interval)return;await this._runWs({type:"maintenance_supporter/task/apply_suggestion",entry_id:this._entryId,task_id:this._taskId,interval:this._task.suggested_interval})&&(this._toast=a("suggestion_applied",this._lang)||"Applied",this._notifyChanged("app Line truncated
`}var Ki,Qi=w(()=>{"use strict";P();j();Ki=["month_jan","month_feb","month_mar","month_apr","month_may","month_jun","month_jul","month_aug","month_sep","month_oct","month_nov","month_dec"]});var I,Ji=w(()=>{"use strict";P();W();j();de();ht();$t();wt();zi();Bi();Vi();Qi();I=class extends A{constructor(){super(...arguments);this._open=!1;this._entryId=null;this._taskId=null;this._task=null;this._objectName="";this._busy=!1;this._error="";this._showSkip=!1;this._showReset=!1;this._showDetails=!1;this._showAdaptive=!1;this._skipReason="";this._resetDate="";this._features={adaptive:!1,predictions:!1,seasonal:!1,environmental:!1,budget:!1,groups:!1,checklists:!1,schedule_time:!1,completion_actions:!1};this._toast="";this._featuresLoaded=!1;this._currencySymbol=""}get _lang(){return D(this.hass)}async openFor(e,t){this._entryId=e,this._taskId=t,this._error="",this._showSkip=!1,this._showReset=!1,this._showAdaptive=!1,this._skipReason="",this._resetDate=le(new Date),this._open=!0,await Promise.all([this._loadTask(),this._loadFeatures()])}async _loadFeatures(){if(!this._featuresLoaded)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/settings"});e?.features&&(this._features={...this._features,...e.features}),this._currencySymbol=e?.budget?.currency_symbol||"",this._featuresLoaded=!0}catch{}}close(){this._open=!1,this._task=null,this._error=""}async _loadTask(){if(!(!this._entryId||!this._taskId))try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId});this._objectName=e.object?.name||"";let t=(e.tasks||[]).find(i=>i.id===this._taskId);this._task=t??null}catch(e){this._error=N(e,this._lang)}}async _runWs(e){this._busy=!0,this._error="";try{return await this.hass.connection.sendMessagePromise(e),this._busy=!1,!0}catch(t){return this._error=N(t,this._lang),this._busy=!1,!1}}_notifyChanged(e){this.dispatchEvent(new CustomEvent("task-action-fired",{detail:{entry_id:this._entryId,task_id:this._taskId,action:e},bubbles:!0,composed:!0}))}_onComplete(){!this._entryId||!this._taskId||!this._task||Promise.resolve().then(()=>(K(),Z)).then(async({openCompleteDialog:e})=>{let t=this._task,i=[];try{i=(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects",compact:!0})).objects||[]}catch{}e(Mi({entryId:this._entryId,taskId:this._taskId,taskName:t.name,task:t,objects:i,lang:this._lang,checklist:t.checklist||[],adaptiveEnabled:!!t.adaptive_config?.enabled,currencySymbol:this._currencySymbol}))&&(this._notifyChanged("complete"),this.close())})}async _onSkipConfirm(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/skip",entry_id:this._entryId,task_id:this._taskId,reason:this._skipReason.trim()||null})&&(this._notifyChanged("skip"),this.close())}async _onResetConfirm(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/reset",entry_id:this._entryId,task_id:this._taskId,date:this._resetDate||void 0})&&(this._notifyChanged("reset"),this.close())}_onEdit(){!this._entryId||!this._taskId||Promise.resolve().then(()=>(K(),Z)).then(({openEditTaskDialog:e})=>{e(this._entryId,this._taskId),this.close()})}_onQr(){!this._entryId||!this._taskId||!this._task||Promise.resolve().then(()=>(K(),Z)).then(({openQrDialog:e})=>{e({entry_id:this._entryId,task_id:this._taskId,task_name:this._task.name,object_name:this._objectName}),this.close()})}async _onDelete(){if(!this._entryId||!this._taskId)return;let e=a("delete_task_confirm",this._lang)||`Delete "${this._task?.name}"?`;if(!window.confirm(e))return;await this._runWs({type:"maintenance_supporter/task/delete",entry_id:this._entryId,task_id:this._taskId})&&(this._notifyChanged("delete"),this.close())}async _onArchive(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/archive",entry_id:this._entryId,task_id:this._taskId})&&(this._notifyChanged("archive"),this.close())}async _onUnarchive(){if(!this._entryId||!this._taskId)return;await this._runWs({type:"maintenance_supporter/task/unarchive",entry_id:this._entryId,task_id:this._taskId})&&(this._notifyChanged("unarchive"),this.close())}_onOpenInPanel(){if(!this._entryId||!this._taskId)return;let e=`/maintenance-supporter?entry_id=${encodeURIComponent(this._entryId)}&task_id=${encodeURIComponent(this._taskId)}`;history.pushState(null,"",e),window.dispatchEvent(new CustomEvent("location-changed")),this.close()}async _applySuggestion(){if(!this._entryId||!this._taskId||!this._task?.suggested_interval)return;await this._runWs({type:"maintenance_supporter/task/apply_suggestion",entry_id:this._entryId,task_id:this._taskId,interval:this._task.suggested_interval})&&(this._toast=a("suggestion_applied",this._lang)||"Applied",this._notifyChanged("apply_suggestion"),await this._loadTask(),setTimeout(()=>{this._toast=""},2500))}async _reanalyzeInterval(){if(!(!this._entryId||!this._taskId)){this._busy=!0,this._error="";tr Line truncated
<div class="recommendation-card">
<h4>${a("suggested_interval",t)}</h4>
${Mi(e.interval_days,e.suggested_interval,e.interval_confidence||"medium",t)}
${Wi(e.interval_days,e.suggested_interval,e.interval_confidence||"medium",t)}
<div class="recommendation-actions">
<button class="btn primary"
@click=${this._applySuggestion} ?disabled=${this._busy}>
@@ -3467,11 +3554,11 @@ ${d?`<div class="sub">${d}</div>`:""}
<div class="adaptive-stack">
${this._toast?l`<div class="toast">${this._toast}</div>`:_}
${i?this._renderRecommendation(e):_}
${n?Hi(e,t,this._features):_}
${o?ji(e,t):_}
${n?Ui(e,t,this._features):_}
${o?Fi(e,t):_}
${p?l`
${Fi(e,t,this._features)}
${e.seasonal_factors?.length===12||e.interval_analysis?.seasonal_factors?.length===12?Ui(e,t):_}
${Gi(e,t,this._features)}
${e.seasonal_factors?.length===12||e.interval_analysis?.seasonal_factors?.length===12?Yi(e,t):_}
`:_}
</div>
`}_renderDetails(e){let t=this._lang,i=e.history||[],n=i.filter(d=>d.type==="completed"),o=n.reduce((d,u)=>d+(typeof u.cost=="number"?u.cost:0),0),p=(()=>{let d=n.map(u=>typeof u.duration=="number"?u.duration:null).filter(u=>u!=null);return d.length?Math.round(d.reduce((u,m)=>u+m,0)/d.length):null})();return l`
@@ -3500,7 +3587,7 @@ ${d?`<div class="sub">${d}</div>`:""}
<div class="history-entry">
<div class="history-line">
<span class="history-type type-${d.type}">${a(d.type,t)}</span>
<span class="history-date">${hi(d.timestamp,t)}</span>
<span class="history-date">${fi(d.timestamp,t)}</span>
${u?l`<button class="history-edit"
title="${a("history_edit_button",t)||"Edit"}"
@click=${()=>this._onEditHistoryEntry(d)}>
@@ -3528,13 +3615,13 @@ ${d?`<div class="sub">${d}</div>`:""}
<span class="task-name">${t.name}</span>
</div>
<div class="object">
<button class="link-inline" @click=${()=>{this._entryId&&Promise.resolve().then(()=>(W(),J)).then(({openObjectQuickActions:n})=>{n(this._entryId),this.close()})}}>${this._objectName}</button>
<button class="link-inline" @click=${()=>{this._entryId&&Promise.resolve().then(()=>(K(),Z)).then(({openObjectQuickActions:n})=>{n(this._entryId),this.close()})}}>${this._objectName}</button>
</div>
<div class="quick-info">
${t.next_due?l`<span><strong>${a("next_due",e)||"Next due"}:</strong> ${Y(t.next_due,e)}</span>`:_}
${t.last_performed?l`<span><strong>${a("last_performed",e)||"Last"}:</strong> ${Y(t.last_performed,e)}</span>`:_}
${t.schedule?.kind&&!["manual","one_time"].includes(t.schedule.kind)||t.interval_days!=null?l`<span><strong>${a("interval",e)||"Interval"}:</strong> ${gi(t,e)}</span>`:_}
${Qe(t)?l`<span><strong>${a("phase_current",e)}:</strong> ${Qe(t)}</span>`:_}
${t.next_due?l`<span><strong>${a("next_due",e)||"Next due"}:</strong> ${Q(t.next_due,e)}</span>`:_}
${t.last_performed?l`<span><strong>${a("last_performed",e)||"Last"}:</strong> ${Q(t.last_performed,e)}</span>`:_}
${t.schedule?.kind&&!["manual","one_time"].includes(t.schedule.kind)||t.interval_days!=null?l`<span><strong>${a("interval",e)||"Interval"}:</strong> ${yi(t,e)}</span>`:_}
${He(t)?l`<span><strong>${a("phase_current",e)}:</strong> ${He(t)}</span>`:_}
</div>
</div>
@@ -3626,7 +3713,7 @@ ${d?`<div class="sub">${d}</div>`:""}
`}
`:l`<div class="loading">${a("loading",e)||"Loading\u2026"}</div>`}
</div>
`}};C.styles=[Ve,S`
`}};I.styles=[Ge,S`
:host { display: contents; }
.backdrop {
position: fixed; inset: 0; z-index: 100;
@@ -3806,7 +3893,7 @@ ${d?`<div class="sub">${d}</div>`:""}
.weibull-section, .seasonal-card-compact { max-width: 100%; }
.weibull-chart svg { max-width: 100%; height: auto; }
.details-toggle { gap: 12px; flex-wrap: wrap; }
`],c([x({attribute:!1})],C.prototype,"hass",2),c([h()],C.prototype,"_open",2),c([h()],C.prototype,"_entryId",2),c([h()],C.prototype,"_taskId",2),c([h()],C.prototype,"_task",2),c([h()],C.prototype,"_objectName",2),c([h()],C.prototype,"_busy",2),c([h()],C.prototype,"_error",2),c([h()],C.prototype,"_showSkip",2),c([h()],C.prototype,"_showReset",2),c([h()],C.prototype,"_showDetails",2),c([h()],C.prototype,"_showAdaptive",2),c([h()],C.prototype,"_skipReason",2),c([h()],C.prototype,"_resetDate",2),c([h()],C.prototype,"_features",2),c([h()],C.prototype,"_toast",2);customElements.get("maintenance-task-quick-actions-dialog")||customElements.define("maintenance-task-quick-actions-dialog",C)});function Vi(r){return!!r&&/^https?:\/\//i.test(r)}var Ki=w(()=>{"use strict"});var V,Gi=w(()=>{"use strict";L();Ki();U();R();le();V=class extends A{constructor(){super(...arguments);this._open=!1;this._entryId=null;this._data=null;this._busy=!1;this._error=""}get _lang(){return j(this.hass)}async openFor(e){this._entryId=e,this._error="",this._open=!0,await this._load()}close(){this._open=!1,this._data=null,this._error=""}async _load(){if(this._entryId)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId});this._data=e}catch(e){this._error=q(e,this._lang)}}_onEditObject(){!this._entryId||!this._data||Promise.resolve().then(()=>(W(),J)).then(({openEditObjectDialog:e})=>{e(this._entryId,this._data.object),this.close()})}_onAddTask(){this._entryId&&Promise.resolve().then(()=>(W(),J)).then(({openCreateTaskDialog:e})=>{e(this._entryId),this.close()})}async _onDelete(){if(!this._entryId||!this._data)return;let e=a("delete_object_confirm",this._lang)||`Delete "${this._data.object.name}" and all its tasks?`;if(window.confirm(e)){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/delete",entry_id:this._entryId}),this.dispatchEvent(new CustomEvent("object-deleted",{detail:{entry_id:this._entryId},bubbles:!0,composed:!0})),this.close()}catch(t){this._error=q(t,this._lang)}finally{this._busy=!1}}}async _onArchiveObject(){if(!this._entryId||!this._data)return;let e=!!this._data.object.archived;if(!e){let t=a("confirm_archive_object",this._lang)||"Archive this object and its tasks?";if(!window.confirm(t))return}this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:e?"maintenance_supporter/object/unarchive":"maintenance_supporter/object/archive",entry_id:this._entryId}),this.dispatchEvent(new CustomEvent("object-changed",{detail:{entry_id:this._entryId},bubbles:!0,composed:!0})),this.close()}catch(t){this._error=q(t,this._lang)}finally{this._busy=!1}}_onTaskClick(e){this._entryId&&Promise.resolve().then(()=>(W(),J)).then(({openTaskQuickActions:t})=>{t(this._entryId,e)})}render(){if(!this._open)return _;let e=this._lang,t=this._data,i=t?.object,n=t?.tasks||[],o=this.hass?.user?.is_admin??!0;return l`
`],c([x({attribute:!1})],I.prototype,"hass",2),c([h()],I.prototype,"_open",2),c([h()],I.prototype,"_entryId",2),c([h()],I.prototype,"_taskId",2),c([h()],I.prototype,"_task",2),c([h()],I.prototype,"_objectName",2),c([h()],I.prototype,"_busy",2),c([h()],I.prototype,"_error",2),c([h()],I.prototype,"_showSkip",2),c([h()],I.prototype,"_showReset",2),c([h()],I.prototype,"_showDetails",2),c([h()],I.prototype,"_showAdaptive",2),c([h()],I.prototype,"_skipReason",2),c([h()],I.prototype,"_resetDate",2),c([h()],I.prototype,"_features",2),c([h()],I.prototype,"_toast",2);customElements.get("maintenance-task-quick-actions-dialog")||customElements.define("maintenance-task-quick-actions-dialog",I)});function Zi(r){return!!r&&/^https?:\/\//i.test(r)}var Xi=w(()=>{"use strict"});function es(r){return r?customElements.get("ha-markdown")?l`<ha-markdown class="notes-md" .content=${r} breaks></ha-markdown>`:l`${r}`:_}var ts=w(()=>{"use strict";P()});var G,is=w(()=>{"use strict";P();Xi();ts();W();j();de();G=class extends A{constructor(){super(...arguments);this._open=!1;this._entryId=null;this._data=null;this._busy=!1;this._error=""}get _lang(){return D(this.hass)}async openFor(e){this._entryId=e,this._error="",this._open=!0,await this._load()}close(){this._open=!1,this._data=null,this._error=""}async _load(){if(this._entryId)try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId});this._data=e}catch(e){this._error=N(e,this._lang)}}_onEditObject(){!this._entryId||!this._data||Promise.resolve().then(()=>(K(),Z)).then(({openEditObjectDialog:e})=>{e(this._entryId,this._data.object),this.close()})}_onAddTask(){this._entryId&&Promise.resolve().then(()=>(K(),Z)).then(({openCreateTaskDialog:e})=>{e(this._entryId),this.close()})}async _onDelete(){if(!this._entryId||!this._data)return;let e=a("delete_object_confirm",this._lang)||`Delete "${this._data.object.name}" and all its tasks?`;if(window.confirm(e)){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/delete",entry_id:this._entryId}),this.dispatchEvent(new CustomEvent("object-deleted",{detail:{entry_id:this._entryId},bubbles:!0,composed:!0})),this.close()}catch(t){this._error=N(t,this._lang)}finally{this._busy=!1}}}async _onArchiveObject(){if(!this._entryId||!this._data)return;let e=!!this._data.object.archived;if(!e){let t=a("confirm_archive_object",this._lang)||"Archive this object and its tasks?";if(!window.confirm(t))return}this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:e?"maintenance_supporter/object/unarchive":"maintenance_supporter/object/archive",entry_id:this._entryId}),this.dispatchEvent(new CustomEvent("object-changed",{detail:{entry_id:this._entryId},bubbles:!0,composed:!0})),this.close()}catch(t){this._error=N(t,this._lang)}finally{this._busy=!1}}_onTaskClick(e){this._entryId&&Promise.resolve().then(()=>(K(),Z)).then(({openTaskQuickActions:t})=>{t(this._entryId,e)})}render(){if(!this._open)return _;let e=this._lang,t=this._data,i=t?.object,n=t?.tasks||[],o=this.hass?.user?.is_admin??!0;return l`
<div class="backdrop" @click=${this.close}></div>
<div class="dialog" role="dialog" aria-modal="true">
${t&&i?l`
@@ -3838,7 +3925,7 @@ ${d?`<div class="sub">${d}</div>`:""}
${i.notes?l`
<div class="notes-section">
<strong>${a("object_notes_label",e)}</strong>
<div class="notes-body">${i.notes}</div>
<div class="notes-body">${es(i.notes)}</div>
</div>
`:_}
@@ -3869,11 +3956,11 @@ ${d?`<div class="sub">${d}</div>`:""}
${i.map(([n,o])=>l`
<div class="meta-item">
<span class="meta-label">${n}</span>
<span class="meta-value">${Vi(o)?l`<a href="${o}" target="_blank" rel="noopener noreferrer">${o}</a>`:o}</span>
<span class="meta-value">${Zi(o)?l`<a href="${o}" target="_blank" rel="noopener noreferrer">${o}</a>`:o}</span>
</div>
`)}
</div>
`}};V.styles=S`
`}};G.styles=S`
:host { display: contents; }
.backdrop {
position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,0.5);
@@ -3916,6 +4003,7 @@ ${d?`<div class="sub">${d}</div>`:""}
.task-name { flex: 1; font-size: 14px; }
.task-status { font-size: 11px; color: var(--secondary-text-color); text-transform: uppercase; }
.notes-body { white-space: pre-wrap; font-size: 13px; padding: 8px; background: var(--secondary-background-color); border-radius: 6px; }
.notes-body ha-markdown { white-space: normal; }
.actions { display: flex; gap: 8px; padding-top: 8px; border-top: 1px solid var(--divider-color); }
.actions .btn { flex: 1; }
.btn {
@@ -3932,7 +4020,7 @@ ${d?`<div class="sub">${d}</div>`:""}
.btn ha-icon { --mdc-icon-size: 16px; }
.loading { padding: 24px; text-align: center; color: var(--secondary-text-color); }
.error { padding: 8px; border-radius: 6px; background: rgba(211,47,47,0.1); color: var(--error-color); font-size: 13px; }
`,c([x({attribute:!1})],V.prototype,"hass",2),c([h()],V.prototype,"_open",2),c([h()],V.prototype,"_entryId",2),c([h()],V.prototype,"_data",2),c([h()],V.prototype,"_busy",2),c([h()],V.prototype,"_error",2);customElements.get("maintenance-object-quick-actions-dialog")||customElements.define("maintenance-object-quick-actions-dialog",V)});var J={};is(J,{openCompleteDialog:()=>cr,openCreateObjectDialog:()=>ar,openCreateTaskDialog:()=>or,openEditObjectDialog:()=>nr,openEditTaskDialog:()=>lr,openHistoryEditDialog:()=>xt,openObjectQuickActions:()=>pr,openQrDialog:()=>dr,openTaskQuickActions:()=>wt});function Ze(){return document.querySelector("home-assistant")?.hass}function rr(){return document.querySelector("home-assistant")?.shadowRoot??document.body}function Z(r){let s=rr(),e=s.querySelector(r)??document.body.querySelector(r);return e?e.parentNode!==s&&s.appendChild(e):(e=document.createElement(r),s.appendChild(e)),e}function X(r){let s=Ze();if(!s)return!1;r.hass=s;let e=j(s);return Re(e)||qe(e).then(()=>{r.requestUpdate?.()}),!0}function Zi(r){return Je||(Je=r.connection.sendMessagePromise({type:"maintenance_supporter/settings"}).then(s=>({features:s.features??Yi.features,defaultWarningDays:s.general?.default_warning_days??7})).catch(()=>Yi),Je)}function ar(){let r=Z(Qi);return X(r)?(r.openCreate(),!0):!1}function nr(r,s){let e=Z(Qi);return X(e)?(e.openEdit(r,s),!0):!1}function or(r="",s){let e=Z(Ji);if(!X(e))return!1;let t=Ze();return t?((async()=>{let i=await Zi(t),n=e;n.checklistsEnabled=i.features.checklists,n.scheduleTimeEnabled=i.features.schedule_time,n.completionActionsEnabled=i.features.completion_actions,n.defaultWarningDays=i.defaultWarningDays,n.openCreate(r,s)})(),!0):!1}function lr(r,s){let e=Z(Ji);if(!X(e))return!1;let t=Ze();return t?((async()=>{try{let[i,n]=await Promise.all([t.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:r}),Zi(t)]),o=(i.tasks||[]).find(d=>d.id===s);if(!o){console.warn(`openEditTaskDialog: task ${s} not found in entry ${r}`);return}let p=e;p.checklistsEnabled=n.features.checklists,p.scheduleTimeEnabled=n.features.schedule_time,p.completionActionsEnabled=n.features.completion_actions,p.defaultWarningDays=n.defaultWarningDays,await p.openEdit(r,o)}catch(i){console.warn("openEditTaskDialog: failed to load task/features",i)}})(),!0):!1}function xt(r){let s=Z(Xs);return X(s)?(s.openEdit(r),!0):!1}function cr(r){let s=Z(er);return X(s)?(s.entryId=r.entry_id,s.taskId=r.task_id,s.taskName=r.task_name,s.checklist=r.checklist??[],s.adaptiveEnabled=!!r.adaptive_enabled,s.requiredFields=r.required_completion_fields??[],s.taskType=r.task_type??"",s.readingUnit=r.reading_unit??"",s.parts=r.parts??[],s.consumesParts=r.consumes_parts??[],s.phaseLabel=r.phase_label??"",s.lang=Ze()?.language||"en",s.open(),!0):!1}function dr(r){let s=Z(tr);return X(s)?(s.openForTask(r.entry_id,r.task_id,r.object_name,r.task_name),!0):!1}function wt(r,s){let e=Z(ir);return X(e)?(e.openFor(r,s),!0):!1}function pr(r){let s=Z(sr);return X(s)?(s.openFor(r),!0):!1}var Qi,Ji,Xs,er,tr,ir,sr,Yi,Je,W=w(()=>{"use strict";yi();Ai();Ti();Ci();Ri();Wi();Gi();R();Qi="maintenance-object-dialog",Ji="maintenance-task-dialog",Xs="maintenance-history-edit-dialog",er="maintenance-complete-dialog",tr="maintenance-qr-dialog",ir="maintenance-task-quick-actions-dialog",sr="maintenance-object-quick-actions-dialog";Yi={features:{adaptive:!1,predictions:!1,seasonal:!1,environmental:!1,budget:!1,groups:!1,checklists:!1,schedule_time:!1,completion_actions:!1},defaultWarningDays:7},Je=null});L();U();ht();L();var si=S`
`,c([x({attribute:!1})],G.prototype,"hass",2),c([h()],G.prototype,"_open",2),c([h()],G.prototype,"_entryId",2),c([h()],G.prototype,"_data",2),c([h()],G.prototype,"_busy",2),c([h()],G.prototype,"_error",2);customElements.get("maintenance-object-quick-actions-dialog")||customElements.define("maintenance-object-quick-actions-dialog",G)});var Z={};ds(Z,{openCompleteDialog:()=>mr,openCreateObjectDialog:()=>ur,openCreateTaskDialog:()=>_r,openEditObjectDialog:()=>hr,openEditTaskDialog:()=>gr,openHistoryEditDialog:()=>kt,openObjectQuickActions:()=>vr,openQrDialog:()=>fr,openTaskQuickActions:()=>Et});function Ze(){return document.querySelector("home-assistant")?.hass}function pr(){return document.querySelector("home-assistant")?.shadowRoot??document.body}function X(r){let s=pr(),e=s.querySelector(r)??document.body.querySelector(r);return e?e.parentNode!==s&&s.appendChild(e):(e=document.createElement(r),s.appendChild(e)),e}function ee(r){let s=Ze();if(!s)return!1;r.hass=s;let e=D(s);return Re(e)||je(e).then(()=>{r.requestUpdate?.()}),!0}function ns(r){return Je||(Je=r.connection.sendMessagePromise({type:"maintenance_supporter/settings"}).then(s=>({features:s.features??ss.features,defaultWarningDays:s.general?.default_warning_days??7})).catch(()=>ss),Je)}function ur(){let r=X(rs);return ee(r)?(r.openCreate(),!0):!1}function hr(r,s){let e=X(rs);return ee(e)?(e.openEdit(r,s),!0):!1}function _r(r="",s){let e=X(as);if(!ee(e))return!1;let t=Ze();return t?((async()=>{let i=await ns(t),n=e;n.checklistsEnabled=i.features.checklists,n.scheduleTimeEnabled=i.features.schedule_time,n.completionActionsEnabled=i.features.completion_actions,n.defaultWarningDays=i.defaultWarningDays,n.openCreate(r,s)})(),!0):!1}function gr(r,s){let e=X(as);if(!ee(e))return!1;let t=Ze();return t?((async()=>{try{let[i,n]=await Promise.all([t.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:r}),ns(t)]),o=(i.tasks||[]).find(d=>d.id===s);if(!o){console.warn(`openEditTaskDialog: task ${s} not found in entry ${r}`);return}let p=e;p.checklistsEnabled=n.features.checklists,p.scheduleTimeEnabled=n.features.schedule_time,p.completionActionsEnabled=n.features.completion_actions,p.defaultWarningDays=n.defaultWarningDays,await p.openEdit(r,o)}catch(i){console.warn("openEditTaskDialog: failed to load task/features",i)}})(),!0):!1}function kt(r){let s=X(nr);return ee(s)?(s.openEdit(r),!0):!1}function mr(r){let s=X(or);return ee(s)?(Oi(s,r,Ze()?.language||"en"),!0):!1}function fr(r){let s=X(lr);return ee(s)?(s.openForTask(r.entry_id,r.task_id,r.object_name,r.task_name),!0):!1}function Et(r,s){let e=X(cr);return ee(e)?(e.openFor(r,s),!0):!1}function vr(r){let s=X(dr);return ee(s)?(s.openFor(r),!0):!1}var rs,as,nr,or,lr,cr,dr,ss,Je,K=w(()=>{"use strict";$i();Li();qi();Ri();Di();Ji();is();j();$t();rs="maintenance-object-dialog",as="maintenance-task-dialog",nr="maintenance-history-edit-dialog",or="maintenance-complete-dialog",lr="maintenance-qr-dialog",cr="maintenance-task-quick-actions-dialog",dr="maintenance-object-quick-actions-dialog";ss={features:{adaptive:!1,predictions:!1,seasonal:!1,environmental:!1,budget:!1,groups:!1,checklists:!1,schedule_time:!1,completion_actions:!1},defaultWarningDays:7},Je=null});P();W();ht();P();var ni=S`
.cal-controls {
display: flex;
gap: 12px;
@@ -4129,35 +4217,35 @@ ${d?`<div class="sub">${d}</div>`:""}
.cal-pill-day { font-size: 17px; }
.cal-user-filter { margin-left: 0; width: 100%; }
}
`;R();function vi(r){let s=window;s.customCards=s.customCards||[],s.customCards.some(e=>e.type===r.type)||s.customCards.push(r)}W();var H=class extends A{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"};this._objects=[];this._stats=null;this._windowDays=30;this._pastDays=0;this._userFilter="";this._objectFilter="";this._configuredObjects=[];this._unsub=null;this._dataLoaded=!1;this._lastConnection=null}static getConfigElement(){return document.createElement("maintenance-supporter-calendar-card-editor")}static getStubConfig(){return{type:"custom:maintenance-supporter-calendar-card",window_days:30,show_window_chips:!0,show_user_filter:!0}}setConfig(e){if(this._config={...e},e.past_days&&[30,90].includes(e.past_days)?this._pastDays=e.past_days:e.window_days&&[7,14,30,365].includes(e.window_days)&&(this._windowDays=e.window_days,this._pastDays=0),typeof e.user_filter=="string"&&(this._userFilter=e.user_filter),typeof e.object_filter=="string")this._objectFilter=e.object_filter,this._configuredObjects=[];else if(Array.isArray(e.object_filter)){let t=e.object_filter.filter(i=>typeof i=="string"&&i!=="");this._objectFilter=t.length===1?t[0]:"",this._configuredObjects=t.length>1?t:[]}}getCardSize(){return 6}get _lang(){return j(this.hass)}disconnectedCallback(){if(super.disconnectedCallback(),this._unsub){try{this._unsub()}catch{}this._unsub=null}this._dataLoaded=!1,this._lastConnection=null}updated(e){if(super.updated(e),pi(this,e),e.has("hass")&&this.hass){if(!this._dataLoaded)this._dataLoaded=!0,this._lastConnection=this.hass.connection,this._loadData(),this._subscribe();else if(this.hass.connection!==this._lastConnection){if(this._lastConnection=this.hass.connection,this._unsub){try{this._unsub()}catch{}this._unsub=null}this._subscribe(),this._loadData()}}}async _loadData(){try{let[e,t]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/statistics"})]);this._objects=e.objects,this._stats=t}catch{}}async _subscribe(){try{let e=await this.hass.connection.subscribeMessage(t=>{let i=t;this._objects=i.objects},{type:"maintenance_supporter/subscribe"});if(!this.isConnected){e();return}this._unsub=e}catch{}}_onEventClick(e){if(e.history_timestamp){this._openHistoryEntry(e);return}wt(e.entry_id,e.task_id)||this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:open-task",entry_id:e.entry_id,task_id:e.task_id},bubbles:!0,composed:!0}))}async _openHistoryEntry(e){try{let i=(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:e.entry_id})).tasks?.find(o=>o.id===e.task_id)?.history?.find(o=>o.timestamp===e.history_timestamp);if(!i||xt({entry_id:e.entry_id,task_id:e.task_id,original_timestamp:e.history_timestamp,type:i.type||"completed",timestamp:i.timestamp||e.history_timestamp,notes:i.notes??null,cost:i.cost??null,duration:i.duration??null,completed_by:i.completed_by??null,used_parts:i.used_parts??null}))return}catch{}this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:edit-history",entry_id:e.entry_id,task_id:e.task_id,original_timestamp:e.history_timestamp},bubbles:!0,composed:!0}))}render(){if(!this.hass)return _;let e=this._lang,t=this._config.show_window_chips!==!1,i=this._config.show_user_filter!==!1,n=this._config.title,o=null;this._userFilter&&(o=this._userFilter==="current_user"?this.hass?.user?.id??null:this._userFilter);let p=y=>{let F=y.toLowerCase();return this._objects.find(I=>I.entry_id===y||I.object.name.toLowerCase()===F)?.entry_id??null},d=new Set(this._configuredObjects.map(p).filter(y=>y!==null)),u=d.size?this._objects.filter(y=>d.has(y.entry_id)):this._objects,m=this._config.show_object_filter!==!1&&u.length>1,f=this._objectFilter?p(this._objectFilter):null,v=f&&u.some(y=>y.entry_id===f)?u.filter(y=>y.entry_id===f):u,b=new Date;b.setHours(0,0,0,0);let k=this._pastDays>0,E=k?ii(v,b,this._pastDays,o):ti(v,b,this._windowDays,o),O=ne(b),K=this._windowDays===365||k,D=K?E.filter(y=>y.events.length>0):E,ce=y=>{let F=`cal-status-${y.status}`,pe=y.projected?"cal-event-projected":"",I=y.status==="overdue"&&y.days_until_due!=null?` (${_i(y.days_until_due,e)})`:"",P=y.projected&&y.interval_days?l`<span class="cal-event-recur">${y.interval_unit&&y.interval_unit!=="days"?`${y.interval_days} ${a("unit_"+y.interval_unit,e)}`:a("cal_every_n_days",e).replace("{n}",String(y.interval_days))}</span>`:_,te=y.schedule_type==="sensor_based",ve=te?l`<ha-icon class="cal-event-icon cal-source-sensor"
`;j();function wi(r){let s=window;s.customCards=s.customCards||[],s.customCards.some(e=>e.type===r.type)||s.customCards.push(r)}K();var O=class extends A{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"};this._objects=[];this._stats=null;this._windowDays=30;this._pastDays=0;this._userFilter="";this._objectFilter="";this._configuredObjects=[];this._unsub=null;this._dataLoaded=!1;this._lastConnection=null}static getConfigElement(){return document.createElement("maintenance-supporter-calendar-card-editor")}static getStubConfig(){return{type:"custom:maintenance-supporter-calendar-card",window_days:30,show_window_chips:!0,show_user_filter:!0}}setConfig(e){if(this._config={...e},e.past_days&&[30,90].includes(e.past_days)?this._pastDays=e.past_days:e.window_days&&[7,14,30,365].includes(e.window_days)&&(this._windowDays=e.window_days,this._pastDays=0),typeof e.user_filter=="string"&&(this._userFilter=e.user_filter),typeof e.object_filter=="string")this._objectFilter=e.object_filter,this._configuredObjects=[];else if(Array.isArray(e.object_filter)){let t=e.object_filter.filter(i=>typeof i=="string"&&i!=="");this._objectFilter=t.length===1?t[0]:"",this._configuredObjects=t.length>1?t:[]}}getCardSize(){return 6}get _lang(){return D(this.hass)}disconnectedCallback(){if(super.disconnectedCallback(),this._unsub){try{this._unsub()}catch{}this._unsub=null}this._dataLoaded=!1,this._lastConnection=null}updated(e){if(super.updated(e),_i(this,e),e.has("hass")&&this.hass){if(!this._dataLoaded)this._dataLoaded=!0,this._lastConnection=this.hass.connection,this._loadData(),this._subscribe();else if(this.hass.connection!==this._lastConnection){if(this._lastConnection=this.hass.connection,this._unsub){try{this._unsub()}catch{}this._unsub=null}this._subscribe(),this._loadData()}}}async _loadData(){try{let[e,t]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/statistics"})]);this._objects=e.objects,this._stats=t}catch{}}async _subscribe(){try{let e=await this.hass.connection.subscribeMessage(t=>{let i=t;this._objects=i.objects},{type:"maintenance_supporter/subscribe"});if(!this.isConnected){e();return}this._unsub=e}catch{}}_onEventClick(e){if(e.history_timestamp){this._openHistoryEntry(e);return}Et(e.entry_id,e.task_id)||this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:open-task",entry_id:e.entry_id,task_id:e.task_id},bubbles:!0,composed:!0}))}async _openHistoryEntry(e){try{let i=(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:e.entry_id})).tasks?.find(o=>o.id===e.task_id)?.history?.find(o=>o.timestamp===e.history_timestamp);if(!i||kt({entry_id:e.entry_id,task_id:e.task_id,original_timestamp:e.history_timestamp,type:i.type||"completed",timestamp:i.timestamp||e.history_timestamp,notes:i.notes??null,cost:i.cost??null,duration:i.duration??null,completed_by:i.completed_by??null,used_parts:i.used_parts??null}))return}catch{}this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:edit-history",entry_id:e.entry_id,task_id:e.task_id,original_timestamp:e.history_timestamp},bubbles:!0,composed:!0}))}render(){if(!this.hass)return _;let e=this._lang,t=this._config.show_window_chips!==!1,i=this._config.show_user_filter!==!1,n=this._config.title,o=null;this._userFilter&&(o=this._userFilter==="current_user"?this.hass?.user?.id??null:this._userFilter);let p=b=>{let B=b.toLowerCase();return this._objects.find(C=>C.entry_id===b||C.object.name.toLowerCase()===B)?.entry_id??null},d=new Set(this._configuredObjects.map(p).filter(b=>b!==null)),u=d.size?this._objects.filter(b=>d.has(b.entry_id)):this._objects,m=this._config.show_object_filter!==!1&&u.length>1,f=this._objectFilter?p(this._objectFilter):null,v=f&&u.some(b=>b.entry_id===f)?u.filter(b=>b.entry_id===f):u,y=new Date;y.setHours(0,0,0,0);let k=this._pastDays>0,E=k?ai(v,y,this._pastDays,o):ri(v,y,this._windowDays,o),H=le(y),M=this._windowDays===365||k,q=M?E.filter(b=>b.events.length>0):E,z=b=>{let B=`cal-status-${b.status}`,pe=b.projected?"cal-event-projected":"",C=b.status==="overdue"&&b.days_until_due!=null?` (${vi(b.days_until_due,e)})`:"",R=b.projected&&b.interval_days?l`<span class="cal-event-recur">${b.interval_unit&&b.interval_unit!=="days"?`${b.interval_days} ${a("unit_"+b.interval_unit,e)}`:a("cal_every_n_days",e).replace("{n}",String(b.interval_days))}</span>`:_,se=b.schedule_type==="sensor_based",ye=se?l`<ha-icon class="cal-event-icon cal-source-sensor"
title="${a("cal_source_sensor",e)}" icon="mdi:trending-up"></ha-icon>`:l`<ha-icon class="cal-event-icon cal-source-time"
title="${y.adaptive_enabled?a("cal_source_time_adaptive",e):a("cal_source_time",e)}"
icon="${y.adaptive_enabled?"mdi:clock-time-four-outline":"mdi:clock-outline"}"></ha-icon>`,ye=te&&y.prediction_confidence&&y.status!=="triggered"&&!y.projected?l`<span class="cal-event-prediction cal-conf-${y.prediction_confidence}">
${a("cal_predicted",e)} · ${a(`cal_confidence_${y.prediction_confidence}`,e)}
</span>`:_,Xi=this._stats?.budget?.currency_symbol||ci,es=y.history_type?a(y.history_type,e):a(y.status,e);return l`
title="${b.adaptive_enabled?a("cal_source_time_adaptive",e):a("cal_source_time",e)}"
icon="${b.adaptive_enabled?"mdi:clock-time-four-outline":"mdi:clock-outline"}"></ha-icon>`,be=se&&b.prediction_confidence&&b.status!=="triggered"&&!b.projected?l`<span class="cal-event-prediction cal-conf-${b.prediction_confidence}">
${a("cal_predicted",e)} · ${a(`cal_confidence_${b.prediction_confidence}`,e)}
</span>`:_,os=this._stats?.budget?.currency_symbol||ui,ls=b.history_type?a(b.history_type,e):a(b.status,e);return l`
<div class="cal-event ${pe}"
@click=${()=>this._onEventClick(y)}>
${ve}
<span class="cal-status-pill ${F}">${es}</span>
@click=${()=>this._onEventClick(b)}>
${ye}
<span class="cal-status-pill ${B}">${ls}</span>
<div class="cal-event-body">
<div class="cal-event-title">${y.object_name} · ${y.task_name}${I}</div>
${ye}
${P}
<div class="cal-event-title">${b.object_name} · ${b.task_name}${C}</div>
${be}
${R}
</div>
${y.avg_cost!=null&&y.avg_cost>0?l`<span class="cal-event-cost">${y.avg_cost.toFixed(0)} ${Xi}</span>`:_}
${b.avg_cost!=null&&b.avg_cost>0?l`<span class="cal-event-cost">${b.avg_cost.toFixed(0)} ${os}</span>`:_}
</div>
`},de=y=>{let[F,pe,I]=y.date.split("-").map(Number),P=new Date(F,pe-1,I),te=y.date===O,ve=P.toLocaleDateString(e,{weekday:"short"}),ye=P.toLocaleDateString(e,{month:"long"});return l`
`},te=b=>{let[B,pe,C]=b.date.split("-").map(Number),R=new Date(B,pe-1,C),se=b.date===H,ye=R.toLocaleDateString(e,{weekday:"short"}),be=R.toLocaleDateString(e,{month:"long"});return l`
<div class="cal-day-row">
<div class="cal-day-pill ${te?"cal-today":""}">
<span class="cal-pill-weekday">${ve}</span>
<span class="cal-pill-day">${P.getDate()}</span>
<div class="cal-day-pill ${se?"cal-today":""}">
<span class="cal-pill-weekday">${ye}</span>
<span class="cal-pill-day">${R.getDate()}</span>
</div>
<div class="cal-day-content">
<div class="cal-day-header">
<span class="cal-day-month">${ye}</span>
${te?l`<span class="cal-day-today-badge">${a("today",e)}</span>`:_}
<span class="cal-day-month">${be}</span>
${se?l`<span class="cal-day-today-badge">${a("today",e)}</span>`:_}
</div>
${y.events.length===0?l`<div class="cal-empty">${a("cal_no_events",e)}</div>`:y.events.map(ce)}
${b.events.length===0?l`<div class="cal-empty">${a("cal_no_events",e)}</div>`:b.events.map(z)}
</div>
</div>
`};return l`
@@ -4166,19 +4254,19 @@ ${d?`<div class="sub">${d}</div>`:""}
<div class="cal-controls">
${t?l`
<div class="cal-window-chips cal-past-chips" title="${a("cal_past_windows",e)||"Past windows"}">
${[30,90].map(y=>l`
<button class="cal-window-chip cal-past-chip ${this._pastDays===y?"active":""}"
@click=${()=>{this._pastDays=y}}>
−${y}d
${[30,90].map(b=>l`
<button class="cal-window-chip cal-past-chip ${this._pastDays===b?"active":""}"
@click=${()=>{this._pastDays=b}}>
−${b}d
</button>
`)}
</div>
<span class="cal-chip-separator" aria-hidden="true">●</span>
<div class="cal-window-chips" title="${a("cal_forward_windows",e)||"Forward windows"}">
${[7,14,30,365].map(y=>l`
<button class="cal-window-chip ${this._pastDays===0&&this._windowDays===y?"active":""}"
@click=${()=>{this._windowDays=y,this._pastDays=0}}>
${y===365?"+1y":`+${y}d`}
${[7,14,30,365].map(b=>l`
<button class="cal-window-chip ${this._pastDays===0&&this._windowDays===b?"active":""}"
@click=${()=>{this._windowDays=b,this._pastDays=0}}>
${b===365?"+1y":`+${b}d`}
</button>
`)}
</div>
@@ -4186,7 +4274,7 @@ ${d?`<div class="sub">${d}</div>`:""}
${i?l`
<select class="cal-user-filter"
.value=${this._userFilter}
@change=${y=>{this._userFilter=y.target.value}}>
@change=${b=>{this._userFilter=b.target.value}}>
<option value="">${a("all_users",e)}</option>
<option value="current_user">${a("my_tasks",e)}</option>
</select>
@@ -4194,21 +4282,21 @@ ${d?`<div class="sub">${d}</div>`:""}
${m?l`
<select class="cal-user-filter"
.value=${f??""}
@change=${y=>{this._objectFilter=y.target.value}}>
@change=${b=>{this._objectFilter=b.target.value}}>
<option value="">${a("all_objects",e)}</option>
${[...u].sort((y,F)=>y.object.name.localeCompare(F.object.name)).map(y=>l`<option value=${y.entry_id} ?selected=${y.entry_id===f}>${y.object.name}</option>`)}
${[...u].sort((b,B)=>b.object.name.localeCompare(B.object.name)).map(b=>l`<option value=${b.entry_id} ?selected=${b.entry_id===f}>${b.object.name}</option>`)}
</select>
`:_}
</div>
`:_}
<div class="cal-rolling">
${D.length===0&&K?l`<div class="cal-empty">${a("cal_no_events",e)}</div>`:D.map(de)}
${q.length===0&&M?l`<div class="cal-empty">${a("cal_no_events",e)}</div>`:q.map(te)}
</div>
</ha-card>
`}};H.styles=[Ve,si,S`
`}};O.styles=[Ge,ni,S`
:host { display: block; }
ha-card { padding: 0; overflow: hidden; }
`],c([x({attribute:!1})],H.prototype,"hass",2),c([h()],H.prototype,"_config",2),c([h()],H.prototype,"_objects",2),c([h()],H.prototype,"_stats",2),c([h()],H.prototype,"_windowDays",2),c([h()],H.prototype,"_pastDays",2),c([h()],H.prototype,"_userFilter",2),c([h()],H.prototype,"_objectFilter",2),c([h()],H.prototype,"_unsub",2);var ur=[{value:7,key:"cal_editor_window_week"},{value:14,key:"cal_editor_window_fortnight"},{value:30,key:"cal_editor_window_month"},{value:365,key:"cal_editor_window_year"}],fe=class extends A{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"}}get _lang(){return j(this.hass)}setConfig(e){this._config={...e}}updated(){let e=this._lang;e&&!Re(e)&&qe(e).then(()=>this.requestUpdate())}_valueChanged(e,t){let i={...this._config,[e]:t};e==="show_window_chips"&&t===!0&&delete i.show_window_chips,e==="show_user_filter"&&t===!0&&delete i.show_user_filter,e==="show_object_filter"&&t===!0&&delete i.show_object_filter,e==="title"&&(!t||typeof t=="string"&&t.trim()==="")&&delete i.title,e==="user_filter"&&t===""&&delete i.user_filter,this._config=i,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:i},bubbles:!0,composed:!0}))}render(){let e=this._lang,t=this._config.window_days??30,i=this._config.show_window_chips!==!1,n=this._config.show_user_filter!==!1,o=this._config.user_filter??"",p=this._config.title??"";return l`
`],c([x({attribute:!1})],O.prototype,"hass",2),c([h()],O.prototype,"_config",2),c([h()],O.prototype,"_objects",2),c([h()],O.prototype,"_stats",2),c([h()],O.prototype,"_windowDays",2),c([h()],O.prototype,"_pastDays",2),c([h()],O.prototype,"_userFilter",2),c([h()],O.prototype,"_objectFilter",2),c([h()],O.prototype,"_unsub",2);var yr=[{value:7,key:"cal_editor_window_week"},{value:14,key:"cal_editor_window_fortnight"},{value:30,key:"cal_editor_window_month"},{value:365,key:"cal_editor_window_year"}],ve=class extends A{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"}}get _lang(){return D(this.hass)}setConfig(e){this._config={...e}}updated(){let e=this._lang;e&&!Re(e)&&je(e).then(()=>this.requestUpdate())}_valueChanged(e,t){let i={...this._config,[e]:t};e==="show_window_chips"&&t===!0&&delete i.show_window_chips,e==="show_user_filter"&&t===!0&&delete i.show_user_filter,e==="show_object_filter"&&t===!0&&delete i.show_object_filter,e==="title"&&(!t||typeof t=="string"&&t.trim()==="")&&delete i.title,e==="user_filter"&&t===""&&delete i.user_filter,this._config=i,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:i},bubbles:!0,composed:!0}))}render(){let e=this._lang,t=this._config.window_days??30,i=this._config.show_window_chips!==!1,n=this._config.show_user_filter!==!1,o=this._config.user_filter??"",p=this._config.title??"";return l`
<div class="editor">
<div class="row">
<label for="title">${a("card_title",e)}</label>
@@ -4225,7 +4313,7 @@ ${d?`<div class="sub">${d}</div>`:""}
id="window"
@change=${d=>this._valueChanged("window_days",Number(d.target.value))}
>
${ur.map(d=>l`<option value="${d.value}" ?selected=${d.value===t}>${a(d.key,e)}</option>`)}
${yr.map(d=>l`<option value="${d.value}" ?selected=${d.value===t}>${a(d.key,e)}</option>`)}
</select>
</div>
<div class="row toggle">
@@ -4270,7 +4358,7 @@ ${d?`<div class="sub">${d}</div>`:""}
</div>
<div class="hint">${a("cal_editor_object_hint",e)}</div>
</div>
`}};fe.styles=S`
`}};ve.styles=S`
:host { display: block; padding: 8px 0; }
.editor { display: flex; flex-direction: column; gap: 12px; }
.row { display: flex; flex-direction: column; gap: 4px; }
@@ -4294,7 +4382,7 @@ ${d?`<div class="sub">${d}</div>`:""}
font-size: 12px;
color: var(--secondary-text-color, #666);
}
`,c([x({attribute:!1})],fe.prototype,"hass",2),c([h()],fe.prototype,"_config",2);customElements.get("maintenance-supporter-calendar-card")||customElements.define("maintenance-supporter-calendar-card",H);customElements.get("maintenance-supporter-calendar-card-editor")||customElements.define("maintenance-supporter-calendar-card-editor",fe);vi({type:"maintenance-supporter-calendar-card",name:"Maintenance Supporter \u2014 Calendar",description:"Rolling calendar of maintenance tasks with 7/14/30/365 day windows, source icons, and prediction-confidence pills.",preview:!0});export{H as MaintenanceCalendarCard};
`,c([x({attribute:!1})],ve.prototype,"hass",2),c([h()],ve.prototype,"_config",2);customElements.get("maintenance-supporter-calendar-card")||customElements.define("maintenance-supporter-calendar-card",O);customElements.get("maintenance-supporter-calendar-card-editor")||customElements.define("maintenance-supporter-calendar-card-editor",ve);wi({type:"maintenance-supporter-calendar-card",name:"Maintenance Supporter \u2014 Calendar",description:"Rolling calendar of maintenance tasks with 7/14/30/365 day windows, source icons, and prediction-confidence pills.",preview:!0});export{O as MaintenanceCalendarCard};
/*! Bundled license information:
@lit/reactive-element/css-tag.js:
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.65.0 */
var S="2.65.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
/*! maintenance_supporter frontend 2.68.0 */
var S="2.68.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
Loaded 100 of 566 files, more files were not shown because too many files have changed in this diff. Show more