diff --git a/custom_components/album_slideshow/camera.py b/custom_components/album_slideshow/camera.py index fbf0d0a2..ae06c94b 100644 --- a/custom_components/album_slideshow/camera.py +++ b/custom_components/album_slideshow/camera.py @@ -1204,6 +1204,12 @@ class AlbumSlideshowCamera(Camera): url, len(data), _MAX_DOWNLOAD_BYTES, ) return None + elif not url.startswith("http"): + # End-to-end encrypted providers (Ente) can't be fetched by URL: + # the coordinator downloads and decrypts the bytes for us. + data = await self._provider_bytes(url) + if data is None: + return None else: data = await self._http_get(url) if data is None: @@ -1212,6 +1218,27 @@ class AlbumSlideshowCamera(Camera): self._download_cache.put(url, data) return data + async def _provider_bytes(self, url: str) -> bytes | None: + """Ask the coordinator for decrypted bytes behind a provider URL.""" + fetcher = getattr(self.coordinator, "async_fetch_image_bytes", None) + if fetcher is None: + _LOGGER.warning("Album Slideshow: no handler for image url %s", url) + return None + try: + data = await fetcher(url) + except Exception as err: # noqa: BLE001 + _LOGGER.warning("Album Slideshow: failed to fetch %s: %s", url, err) + return None + if data is None: + return None + if len(data) > _MAX_DOWNLOAD_BYTES: + _LOGGER.warning( + "Album Slideshow: %s is %d bytes, exceeds %d byte limit; skipping", + url, len(data), _MAX_DOWNLOAD_BYTES, + ) + return None + return data + def _image_request_headers(self, url: str) -> dict[str, str] | None: """Auth headers required to fetch image bytes for some providers. diff --git a/custom_components/album_slideshow/config_flow.py b/custom_components/album_slideshow/config_flow.py index 5ec5d54e..ffd62ddf 100644 --- a/custom_components/album_slideshow/config_flow.py +++ b/custom_components/album_slideshow/config_flow.py @@ -78,9 +78,22 @@ from .const import ( CONF_NEXTCLOUD_FOLDER, CONF_NEXTCLOUD_RECURSIVE, CONF_NEXTCLOUD_IMAGE_SIZE, + CONF_NEXTCLOUD_SHARE_TOKEN, + CONF_NEXTCLOUD_AUTH_MODE, + NEXTCLOUD_AUTH_MODE_FOLDER, + NEXTCLOUD_AUTH_MODE_PUBLIC, + DEFAULT_NEXTCLOUD_AUTH_MODE, DEFAULT_NEXTCLOUD_IMAGE_SIZE, NEXTCLOUD_IMAGE_PREVIEW, NEXTCLOUD_IMAGE_ORIGINAL, + CONF_ENTE_URL, + CONF_ENTE_ACCESS_TOKEN, + CONF_ENTE_COLLECTION_KEY, + CONF_ENTE_API_ORIGIN, + CONF_ENTE_IMAGE_SIZE, + DEFAULT_ENTE_IMAGE_SIZE, + ENTE_IMAGE_FULL, + ENTE_IMAGE_PREVIEW, DEFAULT_REVERSE_GEOCODE, PROVIDER_GOOGLE_SHARED, PROVIDER_LOCAL_FOLDER, @@ -90,6 +103,7 @@ from .const import ( PROVIDER_ICLOUD, PROVIDER_SYNOLOGY, PROVIDER_NEXTCLOUD, + PROVIDER_ENTE, DEFAULT_RECURSIVE, ) @@ -181,6 +195,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if config_entry.data.get(CONF_PROVIDER) in ( PROVIDER_LOCAL_FOLDER, PROVIDER_NEXTCLOUD, + PROVIDER_ENTE, ): return LocalFolderOptionsFlow() return _NoOptionsFlow() @@ -202,6 +217,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_synology() if self._provider == PROVIDER_NEXTCLOUD: return await self.async_step_nextcloud() + if self._provider == PROVIDER_ENTE: + return await self.async_step_ente() return await self.async_step_google_shared() schema = vol.Schema( @@ -213,7 +230,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)", PROVIDER_ICLOUD: "iCloud Shared Album", PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)", - PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder, full metadata)", + PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder or public album link)", + PROVIDER_ENTE: "Ente Photos (public album link)", PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)", }) } @@ -976,6 +994,29 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_nextcloud( self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Pick between an authenticated WebDAV folder and a public album link.""" + if user_input is not None: + if user_input[CONF_NEXTCLOUD_AUTH_MODE] == NEXTCLOUD_AUTH_MODE_PUBLIC: + return await self.async_step_nextcloud_public() + return await self.async_step_nextcloud_folder() + + schema = vol.Schema( + { + vol.Required( + CONF_NEXTCLOUD_AUTH_MODE, default=DEFAULT_NEXTCLOUD_AUTH_MODE + ): vol.In( + { + NEXTCLOUD_AUTH_MODE_FOLDER: "Authenticated WebDAV folder", + NEXTCLOUD_AUTH_MODE_PUBLIC: "Public album link", + } + ), + } + ) + return self.async_show_form(step_id="nextcloud", data_schema=schema) + + async def async_step_nextcloud_folder( + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Collect and validate a Nextcloud WebDAV folder + app password.""" errors: dict[str, str] = {} @@ -1010,6 +1051,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=name, data={ CONF_PROVIDER: PROVIDER_NEXTCLOUD, + CONF_NEXTCLOUD_AUTH_MODE: NEXTCLOUD_AUTH_MODE_FOLDER, CONF_NEXTCLOUD_URL: client.base_url, CONF_NEXTCLOUD_USERNAME: username, CONF_NEXTCLOUD_PASSWORD: password, @@ -1043,7 +1085,138 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) return self.async_show_form( - step_id="nextcloud", data_schema=schema, errors=errors + step_id="nextcloud_folder", data_schema=schema, errors=errors + ) + + async def async_step_nextcloud_public( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Collect and validate a Nextcloud Photos public album link.""" + errors: dict[str, str] = {} + + if user_input is not None: + name = user_input[CONF_ALBUM_NAME].strip() + raw_url = user_input[CONF_NEXTCLOUD_URL].strip() + size = user_input.get( + CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE + ) + + from . import nextcloud as nc_api + + parsed = nc_api.parse_share_link(raw_url) + if parsed is None: + errors[CONF_NEXTCLOUD_URL] = "invalid_nextcloud_url" + else: + base_url, token = parsed + client = nc_api.NextcloudPublicClient(self.hass, base_url, token) + try: + await client.async_validate() + except Exception: # noqa: BLE001 - any failure means bad/expired link + errors["base"] = "nextcloud_public_cannot_connect" + else: + await self.async_set_unique_id( + f"{DOMAIN}:{PROVIDER_NEXTCLOUD}:{base_url}:{token}:{name}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_PROVIDER: PROVIDER_NEXTCLOUD, + CONF_NEXTCLOUD_AUTH_MODE: NEXTCLOUD_AUTH_MODE_PUBLIC, + CONF_NEXTCLOUD_URL: base_url, + CONF_NEXTCLOUD_SHARE_TOKEN: token, + CONF_NEXTCLOUD_IMAGE_SIZE: size, + CONF_ALBUM_NAME: name, + }, + ) + + schema = vol.Schema( + { + vol.Required(CONF_ALBUM_NAME): str, + vol.Required(CONF_NEXTCLOUD_URL): str, + vol.Optional( + CONF_NEXTCLOUD_IMAGE_SIZE, default=DEFAULT_NEXTCLOUD_IMAGE_SIZE + ): vol.In( + { + NEXTCLOUD_IMAGE_PREVIEW: "Preview (smoothest slideshow)", + NEXTCLOUD_IMAGE_ORIGINAL: "Original (full quality, slower)", + } + ), + } + ) + return self.async_show_form( + step_id="nextcloud_public", data_schema=schema, errors=errors + ) + + async def async_step_ente( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Collect and validate an Ente Photos public album link.""" + errors: dict[str, str] = {} + + if user_input is not None: + name = user_input[CONF_ALBUM_NAME].strip() + raw_url = user_input[CONF_ENTE_URL].strip() + api_origin = (user_input.get(CONF_ENTE_API_ORIGIN) or "").strip() + size = user_input.get(CONF_ENTE_IMAGE_SIZE, DEFAULT_ENTE_IMAGE_SIZE) + + from . import ente as ente_api + + share = ente_api.parse_share_link(raw_url) + if share is None: + errors[CONF_ENTE_URL] = "invalid_ente_url" + else: + client = ente_api.EnteClient(self.hass, share, api_origin) + try: + await client.async_validate() + except Exception as err: # noqa: BLE001 - bad, expired or private link + _LOGGER.warning( + "Ente validation failed for %s: %s", + share.album_origin, + _describe_error(err), + ) + errors["base"] = "ente_cannot_connect" + else: + from base64 import b64encode + + await self.async_set_unique_id( + f"{DOMAIN}:{PROVIDER_ENTE}:{share.access_token}:{name}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_PROVIDER: PROVIDER_ENTE, + CONF_ENTE_URL: share.album_origin, + CONF_ENTE_ACCESS_TOKEN: share.access_token, + CONF_ENTE_COLLECTION_KEY: b64encode( + share.collection_key + ).decode("ascii"), + CONF_ENTE_API_ORIGIN: ente_api.normalize_api_origin( + api_origin + ), + CONF_ENTE_IMAGE_SIZE: size, + CONF_ALBUM_NAME: name, + }, + ) + + schema = vol.Schema( + { + vol.Required(CONF_ALBUM_NAME): str, + vol.Required(CONF_ENTE_URL): str, + vol.Optional( + CONF_ENTE_IMAGE_SIZE, default=DEFAULT_ENTE_IMAGE_SIZE + ): vol.In( + { + ENTE_IMAGE_FULL: "Full quality (original file)", + ENTE_IMAGE_PREVIEW: "Preview (faster, softer)", + } + ), + vol.Optional(CONF_ENTE_API_ORIGIN): str, + } + ) + return self.async_show_form( + step_id="ente", data_schema=schema, errors=errors ) diff --git a/custom_components/album_slideshow/const.py b/custom_components/album_slideshow/const.py index 82b91ec7..fcef926f 100644 --- a/custom_components/album_slideshow/const.py +++ b/custom_components/album_slideshow/const.py @@ -59,18 +59,31 @@ PROVIDER_PHOTOPRISM = "photoprism" PROVIDER_ICLOUD = "icloud" PROVIDER_SYNOLOGY = "synology" PROVIDER_NEXTCLOUD = "nextcloud" +PROVIDER_ENTE = "ente" + +# Nextcloud provider - two auth modes against the same PROVIDER_NEXTCLOUD id: +# +# - "folder": authenticated WebDAV folder. Points at any folder in a user's +# files and lists it over WebDAV. Auth is HTTP Basic with a username + app +# password (Settings > Security > Devices & sessions); the app password is +# stored so the coordinator can re-list on each refresh and is sent +# server-side only, never reaching the browser. +# - "public_link": a public Nextcloud Photos album share link. No credentials +# involved - the share token embedded in the URL path is the only thing the +# server checks against the unauthenticated ``photospublic`` WebDAV +# collection. +CONF_NEXTCLOUD_AUTH_MODE = "nextcloud_auth_mode" +NEXTCLOUD_AUTH_MODE_FOLDER = "folder" +NEXTCLOUD_AUTH_MODE_PUBLIC = "public_link" +DEFAULT_NEXTCLOUD_AUTH_MODE = NEXTCLOUD_AUTH_MODE_FOLDER -# Nextcloud (authenticated WebDAV folder) provider. Points at any folder in a -# user's files and lists it over WebDAV. Auth is HTTP Basic with a username + -# app password (Settings > Security > Devices & sessions); the app password is -# stored so the coordinator can re-list on each refresh and is sent server-side -# only, never reaching the browser. CONF_NEXTCLOUD_URL = "nextcloud_url" CONF_NEXTCLOUD_USERNAME = "nextcloud_username" CONF_NEXTCLOUD_PASSWORD = "nextcloud_password" CONF_NEXTCLOUD_FOLDER = "nextcloud_folder" CONF_NEXTCLOUD_RECURSIVE = "nextcloud_recursive" CONF_NEXTCLOUD_IMAGE_SIZE = "nextcloud_image_size" +CONF_NEXTCLOUD_SHARE_TOKEN = "nextcloud_share_token" # ``preview`` uses the core/preview thumbnail endpoint (smoother, smaller); # ``original`` fetches the real file straight off the WebDAV collection. @@ -81,6 +94,25 @@ DEFAULT_NEXTCLOUD_IMAGE_SIZE = NEXTCLOUD_IMAGE_PREVIEW # Long edge (px) requested from the core/preview endpoint for preview quality. NEXTCLOUD_PREVIEW_PX = 1920 +# Ente Photos (public album link) provider. Ente is end-to-end encrypted: the +# access token in the link's ``?t=`` query authenticates to the API, while the +# collection key lives in the link's URL *fragment* and never reaches the +# server. Both are stored so the coordinator can re-list and decrypt on each +# refresh; neither is ever put in an image URL, because Ente images are +# decrypted inside Home Assistant and served from memory. +CONF_ENTE_URL = "ente_url" +CONF_ENTE_ACCESS_TOKEN = "ente_access_token" +CONF_ENTE_COLLECTION_KEY = "ente_collection_key" +CONF_ENTE_API_ORIGIN = "ente_api_origin" +CONF_ENTE_IMAGE_SIZE = "ente_image_size" + +# ``full`` downloads the original file; ``preview`` uses Ente's smaller +# pre-generated thumbnail (much faster, noticeably softer on a big screen). +ENTE_IMAGE_FULL = "full" +ENTE_IMAGE_PREVIEW = "preview" +ENTE_IMAGE_SIZE_OPTIONS = [ENTE_IMAGE_FULL, ENTE_IMAGE_PREVIEW] +DEFAULT_ENTE_IMAGE_SIZE = ENTE_IMAGE_FULL + # Synology Photos (direct API) provider. Talks to a DSM Photos package over its # entry.cgi web API. The account password is stored so the coordinator can # re-authenticate when the session id expires; accounts with 2FA are handled by diff --git a/custom_components/album_slideshow/coordinator.py b/custom_components/album_slideshow/coordinator.py index 08dd588d..1238d612 100644 --- a/custom_components/album_slideshow/coordinator.py +++ b/custom_components/album_slideshow/coordinator.py @@ -65,9 +65,20 @@ from .const import ( CONF_NEXTCLOUD_FOLDER, CONF_NEXTCLOUD_RECURSIVE, CONF_NEXTCLOUD_IMAGE_SIZE, + CONF_NEXTCLOUD_SHARE_TOKEN, + CONF_NEXTCLOUD_AUTH_MODE, + NEXTCLOUD_AUTH_MODE_PUBLIC, + DEFAULT_NEXTCLOUD_AUTH_MODE, DEFAULT_NEXTCLOUD_IMAGE_SIZE, NEXTCLOUD_IMAGE_ORIGINAL, NEXTCLOUD_PREVIEW_PX, + CONF_ENTE_URL, + CONF_ENTE_ACCESS_TOKEN, + CONF_ENTE_COLLECTION_KEY, + CONF_ENTE_API_ORIGIN, + CONF_ENTE_IMAGE_SIZE, + DEFAULT_ENTE_IMAGE_SIZE, + ENTE_IMAGE_PREVIEW, DEFAULT_REVERSE_GEOCODE, DOMAIN, PROVIDER_GOOGLE_SHARED, @@ -78,6 +89,7 @@ from .const import ( PROVIDER_ICLOUD, PROVIDER_SYNOLOGY, PROVIDER_NEXTCLOUD, + PROVIDER_ENTE, ) from .store import SlideshowStore @@ -883,6 +895,55 @@ def _read_manifest_version(integration_dir: Path) -> str: return "" +def _build_ente_item( + raw: dict[str, Any], collection_key: bytes, want_preview: bool +) -> tuple[MediaItem, dict[str, Any]] | None: + """Decrypt one Ente file into a MediaItem plus its fetch material. + + Runs in an executor: unsealing a key and opening two small secretstreams + is CPU work, and an album can hold thousands of files. Returns ``None`` + for anything that is not a still image. + """ + from . import ente as ente_api + + file_key = ente_api.decrypt_file_key(raw, collection_key) + metadata = ente_api.decrypt_metadata(raw, file_key) + if not ente_api.is_image(metadata): + return None + + magic = ente_api.decrypt_magic_metadata(raw, file_key) + fields = ente_api.build_media_fields(metadata, magic) + + attrs = raw.get("thumbnail" if want_preview else "file") + if not isinstance(attrs, dict) or not attrs.get("decryptionHeader"): + return None + info = raw.get("info") if isinstance(raw.get("info"), dict) else {} + + item = MediaItem( + url=ente_api.build_item_url(raw["id"]), + width=fields["width"], + height=fields["height"], + mime_type=None, + filename=fields["filename"], + captured_at=fields["captured_at"], + uploaded_at=fields["uploaded_at"], + byte_size=info.get("thumbSize" if want_preview else "fileSize"), + latitude=fields["latitude"], + longitude=fields["longitude"], + description=fields["description"], + source_id=str(raw["id"]), + # Ente hands over full metadata up front, so there is nothing for the + # per-photo enrichment download to add. + exif_scanned=True, + ) + meta = { + "key": file_key, + "header": attrs["decryptionHeader"], + "thumbnail": want_preview, + } + return item, meta + + def _merge_prior_enrichment( new_items: list[MediaItem], prior_items: list[MediaItem] ) -> None: @@ -938,6 +999,9 @@ class AlbumCoordinator(DataUpdateCoordinator): # Extra headers the camera must send when fetching image bytes # (Immich API key). Empty for providers that need no auth. self.image_request_headers: dict[str, str] = {} + # Ente only: file id -> {key, header, thumbnail}, the material needed + # to decrypt each image. Rebuilt on every album refresh. + self._ente_fetch_meta: dict[str, dict[str, Any]] = {} # Persist the most recent successful album fetch so that a transient # network/Google failure doesn't blank the slideshow on restart. @@ -1011,6 +1075,8 @@ class AlbumCoordinator(DataUpdateCoordinator): data = await self._update_synology() elif self.provider == PROVIDER_NEXTCLOUD: data = await self._update_nextcloud() + elif self.provider == PROVIDER_ENTE: + data = await self._update_ente() else: raise UpdateFailed(f"Unsupported provider: {self.provider}") except UpdateFailed: @@ -1024,7 +1090,12 @@ class AlbumCoordinator(DataUpdateCoordinator): raise items = data.get("items") or [] - if self.provider in (PROVIDER_LOCAL_FOLDER, PROVIDER_IMMICH, PROVIDER_NEXTCLOUD) and items: + if self.provider in ( + PROVIDER_LOCAL_FOLDER, + PROVIDER_IMMICH, + PROVIDER_NEXTCLOUD, + PROVIDER_ENTE, + ) and items: # Carry forward EXIF/geocode metadata for items we've already # scanned this session; new items get filled in by the # background worker below. @@ -1066,7 +1137,13 @@ class AlbumCoordinator(DataUpdateCoordinator): """Kick off the background EXIF + geocode worker if there's work.""" items: list[MediaItem] = data.get("items") or [] unscanned = [it for it in items if not it.exif_scanned] - if not unscanned: + # Providers that decrypt/return GPS inline (Ente) have nothing to scan + # but still need the coordinates turned into a place label. + needs_geocode = any( + it.latitude is not None and it.longitude is not None and not it.location + for it in items + ) + if not unscanned and not needs_geocode: # Even with nothing to do, mark the phase as ``done`` so the # diagnostic sensor stops reporting an in-progress run from # the previous refresh. @@ -1736,6 +1813,118 @@ class AlbumCoordinator(DataUpdateCoordinator): } async def _update_nextcloud(self) -> dict[str, Any]: + """List photos from Nextcloud, dispatching on the configured auth mode.""" + mode = self.entry.data.get( + CONF_NEXTCLOUD_AUTH_MODE, DEFAULT_NEXTCLOUD_AUTH_MODE + ) + if mode == NEXTCLOUD_AUTH_MODE_PUBLIC: + return await self._update_nextcloud_public() + return await self._update_nextcloud_folder() + + def _ente_share(self): + """Rebuild the parsed share (token + collection key) from entry data.""" + from base64 import b64decode + + from . import ente as ente_api + + token = self.entry.data.get(CONF_ENTE_ACCESS_TOKEN) + raw_key = self.entry.data.get(CONF_ENTE_COLLECTION_KEY) + if not token or not raw_key: + raise UpdateFailed("Ente entry is missing its link credentials") + return ente_api.EnteShare( + token, + b64decode(raw_key), + self.entry.data.get(CONF_ENTE_URL) or "", + ) + + async def _update_ente(self) -> dict[str, Any]: + """List and decrypt an Ente public album. + + Ente returns everything encrypted, so each file's key is unsealed from + the collection key and its metadata decrypted here. That metadata is + complete (capture date, GPS, caption), so unlike the folder-style + providers there is no per-photo enrichment download; items come back + already scanned and only need reverse-geocoding. + """ + from . import ente as ente_api + + share = self._ente_share() + client = ente_api.EnteClient( + self.hass, share, self.entry.data.get(CONF_ENTE_API_ORIGIN) + ) + + try: + files = await client.async_list_files() + except Exception as err: + raise UpdateFailed(f"Error listing Ente album: {err}") from err + + want_preview = ( + self.entry.data.get(CONF_ENTE_IMAGE_SIZE, DEFAULT_ENTE_IMAGE_SIZE) + == ENTE_IMAGE_PREVIEW + ) + + items: list[MediaItem] = [] + fetch_meta: dict[str, dict[str, Any]] = {} + for f in files: + try: + built = await self.hass.async_add_executor_job( + _build_ente_item, f, share.collection_key, want_preview + ) + except Exception as err: # noqa: BLE001 - one bad file must not fail the album + _LOGGER.debug("Ente: skipping file %s (%s)", f.get("id"), err) + continue + if built is None: + continue + item, meta = built + items.append(item) + fetch_meta[str(f["id"])] = meta + + if not items: + raise UpdateFailed("No images found in the Ente album") + + # Per-file keys + stream headers, so the camera can decrypt on demand + # without re-listing the album for every slide. + self._ente_fetch_meta = fetch_meta + + return { + "title": self.entry.title, + "items": items, + } + + async def async_fetch_image_bytes(self, url: str) -> bytes | None: + """Fetch and decrypt bytes for a provider-internal URL. + + Only Ente uses this today: its images cannot be served by URL because + the bytes are end-to-end encrypted, so the camera hands the synthetic + ``ente://`` id back here to be downloaded and decrypted. + Returns ``None`` for anything this coordinator does not own. + """ + from . import ente as ente_api + + if self.provider != PROVIDER_ENTE: + return None + file_id = ente_api.file_id_from_url(url) + if not file_id: + return None + + meta = (self._ente_fetch_meta or {}).get(str(file_id)) + if not meta: + _LOGGER.debug("Ente: no decryption material cached for %s", file_id) + return None + + share = self._ente_share() + client = ente_api.EnteClient( + self.hass, share, self.entry.data.get(CONF_ENTE_API_ORIGIN) + ) + try: + return await client.async_fetch_image( + file_id, meta["key"], meta["header"], meta["thumbnail"] + ) + except Exception as err: # noqa: BLE001 + _LOGGER.warning("Ente: failed to fetch image %s: %s", file_id, err) + return None + + async def _update_nextcloud_folder(self) -> dict[str, Any]: """List photos from an authenticated Nextcloud WebDAV folder. The PROPFIND listing carries filename/size/content-type/mtime but no @@ -1799,39 +1988,85 @@ class AlbumCoordinator(DataUpdateCoordinator): "items": items, } + async def _update_nextcloud_public(self) -> dict[str, Any]: + """List photos from a Nextcloud Photos public collaborative album. + + The PROPFIND listing carries filename/size/content-type/mtime but no + EXIF, so capture date, GPS and description are filled in afterwards + by the background enrichment worker (one original-file download per + photo - Nextcloud has no metadata-only endpoint the way Immich does). + No credentials are involved - the share token is embedded in the URL. + """ + from . import nextcloud as nc_api + + url = self.entry.data.get(CONF_NEXTCLOUD_URL) + token = self.entry.data.get(CONF_NEXTCLOUD_SHARE_TOKEN) + size = self.entry.data.get( + CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE + ) + if not url or not token: + raise UpdateFailed("Nextcloud provider is missing the URL or share token") + + client = nc_api.NextcloudPublicClient(self.hass, url, token) + try: + photos = await client.async_list_photos() + except Exception as err: + raise UpdateFailed(f"Error listing Nextcloud album: {err}") from err + + if not photos: + raise UpdateFailed("No images found in the Nextcloud album") + + items: list[MediaItem] = [] + for p in photos: + href = p.get("href") + if not href: + continue + if size != NEXTCLOUD_IMAGE_ORIGINAL and p.get("file_id"): + display_url = nc_api.build_preview_url_public( + client.base_url, token, p["file_id"], NEXTCLOUD_PREVIEW_PX + ) + else: + display_url = href + items.append( + MediaItem( + url=display_url, + width=None, + height=None, + mime_type=p.get("content_type"), + filename=p.get("display_name") or p.get("filename"), + uploaded_at=p.get("mtime_ms"), + byte_size=p.get("size"), + # The DAV href, so enrichment can fetch the original bytes + # even when the display URL is a preview. + source_id=href, + ) + ) + + return { + "title": self.entry.title, + "items": items, + } + async def _enrich_nextcloud_item(self, item: MediaItem) -> None: """Download one Nextcloud photo's original bytes and read its EXIF. - Nextcloud's WebDAV folder has no metadata-only endpoint (unlike + Nextcloud has no metadata-only endpoint for either auth mode (unlike Immich's per-asset detail call), so enrichment costs one full-file download per photo regardless of the display quality configured. + Dispatches on the configured auth mode to build the right download + URL/headers; the download-and-parse tail is identical either way. """ - from . import nextcloud as nc_api - from urllib.parse import quote - - url = self.entry.data.get(CONF_NEXTCLOUD_URL) - username = self.entry.data.get(CONF_NEXTCLOUD_USERNAME) - password = self.entry.data.get(CONF_NEXTCLOUD_PASSWORD) - folder = self.entry.data.get(CONF_NEXTCLOUD_FOLDER) or "" - if not username or not password or not url: - item.exif_scanned = True - return - - # Reconstruct the original-file URL: for preview items the display url - # is the preview endpoint, so fall back to the folder href by filename. - original_url = None - if isinstance(item.url, str) and "/remote.php/dav/files/" in item.url: - original_url = item.url - elif item.filename: - client = nc_api.NextcloudClient(self.hass, url, username, password, folder) - original_url = client.dav_root + quote(item.filename) + mode = self.entry.data.get( + CONF_NEXTCLOUD_AUTH_MODE, DEFAULT_NEXTCLOUD_AUTH_MODE + ) + if mode == NEXTCLOUD_AUTH_MODE_PUBLIC: + original_url, headers = self._nextcloud_public_original(item) + else: + original_url, headers = self._nextcloud_folder_original(item) if not original_url: item.exif_scanned = True return - headers = { - "Authorization": nc_api.basic_auth_header(username, password) - } session = async_get_clientsession(self.hass) try: async with async_timeout.timeout(30): @@ -1872,6 +2107,50 @@ class AlbumCoordinator(DataUpdateCoordinator): item.longitude = info["longitude"] item.exif_scanned = True + def _nextcloud_folder_original( + self, item: MediaItem + ) -> tuple[str | None, dict[str, str]]: + """Return ``(original_url, headers)`` for a folder-mode item, or (None, {}).""" + from . import nextcloud as nc_api + from urllib.parse import quote + + url = self.entry.data.get(CONF_NEXTCLOUD_URL) + username = self.entry.data.get(CONF_NEXTCLOUD_USERNAME) + password = self.entry.data.get(CONF_NEXTCLOUD_PASSWORD) + folder = self.entry.data.get(CONF_NEXTCLOUD_FOLDER) or "" + if not username or not password or not url: + return None, {} + + # Reconstruct the original-file URL: for preview items the display url + # is the preview endpoint, so fall back to the folder href by filename. + original_url = None + if isinstance(item.url, str) and "/remote.php/dav/files/" in item.url: + original_url = item.url + elif item.filename: + client = nc_api.NextcloudClient(self.hass, url, username, password, folder) + original_url = client.dav_root + quote(item.filename) + if not original_url: + return None, {} + return original_url, {"Authorization": nc_api.basic_auth_header(username, password)} + + def _nextcloud_public_original( + self, item: MediaItem + ) -> tuple[str | None, dict[str, str]]: + """Return ``(original_url, headers)`` for a public-link item, or (None, {}).""" + url = self.entry.data.get(CONF_NEXTCLOUD_URL) + token = self.entry.data.get(CONF_NEXTCLOUD_SHARE_TOKEN) + if not url or not token: + return None, {} + + # For preview items the display url is the preview endpoint, so fall + # back to the DAV href stashed on the item at listing time. + for candidate in (item.url, item.source_id): + if isinstance(candidate, str) and ( + "/remote.php/dav/photospublic/" in candidate + ): + return candidate, {} + return None, {} + async def _enrich_immich_item(self, item: MediaItem) -> None: """Fetch one Immich asset's detail and fill location/description.""" from . import immich as immich_api diff --git a/custom_components/album_slideshow/manifest.json b/custom_components/album_slideshow/manifest.json index ac13cf93..e9441570 100644 --- a/custom_components/album_slideshow/manifest.json +++ b/custom_components/album_slideshow/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://github.com/eyalgal/album_slideshow", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/eyalgal/album_slideshow/issues", - "requirements": ["Pillow"], - "version": "1.8.1" + "requirements": ["Pillow", "PyNaCl>=1.5.0"], + "version": "1.9.0" } diff --git a/custom_components/album_slideshow/strings.json b/custom_components/album_slideshow/strings.json index 836a6681..41af6598 100644 --- a/custom_components/album_slideshow/strings.json +++ b/custom_components/album_slideshow/strings.json @@ -111,6 +111,13 @@ } }, "nextcloud": { + "title": "Nextcloud", + "description": "Choose how to connect: an authenticated WebDAV folder in your Nextcloud files, or a public Nextcloud Photos album share link.", + "data": { + "nextcloud_auth_mode": "Connection type" + } + }, + "nextcloud_folder": { "title": "Nextcloud folder", "description": "Connect to a folder in your Nextcloud files over WebDAV. Enter the server address (e.g. http://192.168.1.10, or your Nextcloud domain over HTTPS), your username, and an app password (create one under Settings > Security > Devices and sessions - not your main login password). Point it at a folder path (e.g. Photos/Family), or leave the folder blank for your whole files root. Turn on recursive to include subfolders.", "data": { @@ -122,6 +129,25 @@ "nextcloud_recursive": "Include subfolders", "nextcloud_image_size": "Image quality" } + }, + "ente": { + "title": "Ente Photos public album", + "description": "Paste a public Ente album link, for example https://albums.ente.io/?t=TOKEN#KEY. Copy the whole link including the part after the # - that fragment is the decryption key and the album cannot be read without it. No Ente account or password is needed. Leave the API endpoint blank unless you self-host Ente.", + "data": { + "album_name": "Album name", + "ente_url": "Album share link", + "ente_image_size": "Image quality", + "ente_api_origin": "API endpoint (self-hosted only)" + } + }, + "nextcloud_public": { + "title": "Nextcloud public album link", + "description": "Paste a public Nextcloud Photos album share link (from the Photos app's Share menu). No login is needed - the link itself grants access.", + "data": { + "album_name": "Album name", + "nextcloud_url": "Album share link", + "nextcloud_image_size": "Image quality" + } } }, "error": { @@ -143,7 +169,11 @@ "synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.", "synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue.", "synology_shared_unavailable": "Could not access the Shared Space. Enable Shared Space in Synology Photos and make sure this account has access to it, or choose Personal instead.", - "nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path." + "nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path.", + "invalid_nextcloud_url": "That does not look like a Nextcloud Photos public album link.", + "nextcloud_public_cannot_connect": "Could not connect to that Nextcloud album. Check the link is still valid.", + "invalid_ente_url": "That does not look like an Ente public album link. It should look like https://albums.ente.io/?t=TOKEN#KEY, including the part after the #." + ,"ente_cannot_connect": "Could not open that Ente album. The link may be expired, revoked, or password-protected." } }, "options": { diff --git a/custom_components/album_slideshow/translations/en.json b/custom_components/album_slideshow/translations/en.json index 836a6681..41af6598 100644 --- a/custom_components/album_slideshow/translations/en.json +++ b/custom_components/album_slideshow/translations/en.json @@ -111,6 +111,13 @@ } }, "nextcloud": { + "title": "Nextcloud", + "description": "Choose how to connect: an authenticated WebDAV folder in your Nextcloud files, or a public Nextcloud Photos album share link.", + "data": { + "nextcloud_auth_mode": "Connection type" + } + }, + "nextcloud_folder": { "title": "Nextcloud folder", "description": "Connect to a folder in your Nextcloud files over WebDAV. Enter the server address (e.g. http://192.168.1.10, or your Nextcloud domain over HTTPS), your username, and an app password (create one under Settings > Security > Devices and sessions - not your main login password). Point it at a folder path (e.g. Photos/Family), or leave the folder blank for your whole files root. Turn on recursive to include subfolders.", "data": { @@ -122,6 +129,25 @@ "nextcloud_recursive": "Include subfolders", "nextcloud_image_size": "Image quality" } + }, + "ente": { + "title": "Ente Photos public album", + "description": "Paste a public Ente album link, for example https://albums.ente.io/?t=TOKEN#KEY. Copy the whole link including the part after the # - that fragment is the decryption key and the album cannot be read without it. No Ente account or password is needed. Leave the API endpoint blank unless you self-host Ente.", + "data": { + "album_name": "Album name", + "ente_url": "Album share link", + "ente_image_size": "Image quality", + "ente_api_origin": "API endpoint (self-hosted only)" + } + }, + "nextcloud_public": { + "title": "Nextcloud public album link", + "description": "Paste a public Nextcloud Photos album share link (from the Photos app's Share menu). No login is needed - the link itself grants access.", + "data": { + "album_name": "Album name", + "nextcloud_url": "Album share link", + "nextcloud_image_size": "Image quality" + } } }, "error": { @@ -143,7 +169,11 @@ "synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.", "synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue.", "synology_shared_unavailable": "Could not access the Shared Space. Enable Shared Space in Synology Photos and make sure this account has access to it, or choose Personal instead.", - "nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path." + "nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path.", + "invalid_nextcloud_url": "That does not look like a Nextcloud Photos public album link.", + "nextcloud_public_cannot_connect": "Could not connect to that Nextcloud album. Check the link is still valid.", + "invalid_ente_url": "That does not look like an Ente public album link. It should look like https://albums.ente.io/?t=TOKEN#KEY, including the part after the #." + ,"ente_cannot_connect": "Could not open that Ente album. The link may be expired, revoked, or password-protected." } }, "options": { diff --git a/custom_components/album_slideshow/www/album-slideshow-card.js b/custom_components/album_slideshow/www/album-slideshow-card.js index c80ef8c1..598198ce 100644 --- a/custom_components/album_slideshow/www/album-slideshow-card.js +++ b/custom_components/album_slideshow/www/album-slideshow-card.js @@ -26,7 +26,7 @@ * tap_action: none # none | more-info */ -const VERSION = "1.8.1"; +const VERSION = "1.9.0"; const ANIMATED_TRANSITIONS = [ "fade", @@ -810,9 +810,11 @@ function createAlbumSlideshowCardClass(Base) { ddd: part({ weekday: "short" }), HH: pad(d.getHours()), mm: pad(d.getMinutes()), + // Lets relative time be mixed into a format string, e.g. "DD MMMM YYYY - REL". + REL: this._relativeTime(d), }; return fmt.replace( - /YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|mm/g, + /REL|YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|mm/g, (t) => map[t], ); } @@ -925,6 +927,10 @@ const CAPTION_DATE_FORMAT_OPTIONS = [ { value: "numeric", label: "Numeric (8/16/2014)" }, { value: "weekday", label: "Weekday (Saturday, August 16, 2014)" }, { value: "relative", label: "Relative (3 years ago)" }, + { + value: "D MMMM YYYY - REL", + label: "Date + relative (16 August 2014 - 3 years ago)", + }, ]; const DEFAULTS = { @@ -1440,7 +1446,7 @@ function createAlbumSlideshowCardEditorClass(Base) { tap_pause_seconds: "How long the card freezes its slide after a tap. 0 disables it.", caption_date_format: - "Pick a preset or type a custom format (YYYY, MMMM, MMM, MM, DD, D).", + "Pick a preset or type a custom format (YYYY, MMMM, MMM, MM, DD, D, REL for relative time).", caption_show: "Description comes from the photo's EXIF/IPTC/XMP caption and is only available with the local-folder provider.", caption_per_image: diff --git a/custom_components/maintenance_supporter/__init__.py b/custom_components/maintenance_supporter/__init__.py index 89fd7a0a..f05aa56f 100644 --- a/custom_components/maintenance_supporter/__init__.py +++ b/custom_components/maintenance_supporter/__init__.py @@ -1147,7 +1147,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: rotation tasks that were configured without an initial assignee — those were invisible to every user filter until their first completion. """ - if entry.version > 1 or entry.minor_version >= 5: + if entry.version > 1 or entry.minor_version >= 6: return True is_object_entry = entry.unique_id != GLOBAL_UNIQUE_ID @@ -1225,7 +1225,28 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: shed_owned_devices(hass, own_entry_id=entry.entry_id, source_device_id=device_id) minor = 5 - hass.config_entries.async_update_entry(entry, data=data, minor_version=minor) + # 5 → 6 (#145): task rows show Complete / Skip as HA buttons instead of + # bare icons. An EXISTING install gets a one-time panel notice with a + # "back to icons" button (the flag lives in the hub entry's options and + # is cleared by that banner); a fresh install starts at minor 6 and + # never sees it. + new_options: dict[str, Any] | None = None + if minor < 6: + if not is_object_entry: + from .const import CONF_ROW_ACTION_NOTICE + + # Older installs keep their settings in entry.data with EMPTY + # options (every reader uses `entry.options or entry.data`); the + # first options write must carry them over, or the flag alone + # would mask every setting. + base = dict(entry.options) if entry.options else dict(entry.data) + new_options = {**base, CONF_ROW_ACTION_NOTICE: True} + minor = 6 + + if new_options is not None: + hass.config_entries.async_update_entry(entry, data=data, options=new_options, minor_version=minor) + else: + hass.config_entries.async_update_entry(entry, data=data, minor_version=minor) _LOGGER.info("Migrated entry %s to minor_version %s", entry.entry_id, minor) return True @@ -1445,6 +1466,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf entry.runtime_data = MaintenanceSupporterData(coordinator=coordinator, store=store) await coordinator.async_config_entry_first_refresh() + # #148: a battery type that becomes known only after fleet setup (a + # Battery Notes note added later, a typed low-only Matter binary) + # never got its spare part — reconcile once per start, after the + # other integrations' entities exist. Also prunes the legacy + # untouched "UNKNOWN battery" part older versions minted. Registered + # AFTER runtime_data: on a running instance async_at_started fires + # immediately, and the reconcile needs the store for stock/prune. + if obj_data.get(BATTERY_FLEET_OBJECT_FLAG): + + async def _fleet_parts_at_start(_hass: HomeAssistant) -> None: + from .helpers.battery_fleet_setup import reconcile_fleet_parts_at_start + from .helpers.i18n import normalize_language + + try: + result = await reconcile_fleet_parts_at_start(hass, entry, normalize_language(hass)) + except Exception: + _LOGGER.exception("Battery-fleet part reconcile at start failed") + return + if result["added"] or result["pruned"]: + _LOGGER.info( + "Battery-fleet parts reconciled at start: added %s, pruned legacy UNKNOWN: %s", + result["added"] or "none", + result["pruned"], + ) + + entry.async_on_unload(async_at_started(hass, _fleet_parts_at_start)) + # v1.5.3 (#48): forward-sync obj fields → device_registry whenever the # entry data changes (WS update OR config-flow re-edit). DeviceInfo # only seeds these on first device creation; after that, dashboard diff --git a/custom_components/maintenance_supporter/config_flow.py b/custom_components/maintenance_supporter/config_flow.py index f3734e87..a3a85f39 100644 --- a/custom_components/maintenance_supporter/config_flow.py +++ b/custom_components/maintenance_supporter/config_flow.py @@ -63,7 +63,7 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con """Handle a config flow for Maintenance Supporter.""" VERSION = 1 - MINOR_VERSION = 5 + MINOR_VERSION = 6 def __init__(self) -> None: """Initialize the config flow.""" diff --git a/custom_components/maintenance_supporter/const.py b/custom_components/maintenance_supporter/const.py index 2702c61a..229b2caf 100644 --- a/custom_components/maintenance_supporter/const.py +++ b/custom_components/maintenance_supporter/const.py @@ -122,6 +122,23 @@ CONF_PANEL_ENABLED = "panel_enabled" # still honoured. Read this default everywhere instead of inlining the literal. DEFAULT_PANEL_ENABLED = True CONF_PANEL_TITLE = "panel_title" +# #145: how task rows show Complete / Skip — HA buttons that collapse to +# icon-only on phones (default), labelled buttons everywhere, or the classic +# icon pair. The notice flag is set ONCE by the 5→6 migration for existing +# installs so the panel can announce the new look with a "back to icons" button. +CONF_ROW_ACTION_STYLE = "row_action_style" +CONF_ROW_ACTION_NOTICE = "row_action_notice_pending" +ROW_ACTION_STYLES: tuple[str, ...] = ("buttons_compact", "buttons", "icons") +DEFAULT_ROW_ACTION_STYLE = "buttons_compact" +# #146: household defaults for the "low" thresholds that discovery and the +# battery fleet used to hard-code. Consumables: the catalog floor for +# percent-remaining signatures (Suggested setups). Batteries: the fleet-wide +# floor for batteries without their own Battery Notes threshold. +CONF_DEFAULT_CONSUMABLE_THRESHOLD = "default_consumable_threshold" +CONF_BATTERY_LOW_PERCENT = "battery_low_percent" +DEFAULT_CONSUMABLE_THRESHOLD = 10 +DEFAULT_BATTERY_LOW_PERCENT = 20 +THRESHOLD_PERCENT_RANGE: tuple[int, int] = (1, 90) # --- Config Keys: Advanced Feature Visibility --- CONF_ADVANCED_ADAPTIVE = "advanced_adaptive_visible" diff --git a/custom_components/maintenance_supporter/frontend-src/components/settings-view.ts b/custom_components/maintenance_supporter/frontend-src/components/settings-view.ts index 3e2a7b71..66bf4520 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/settings-view.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/settings-view.ts @@ -43,6 +43,19 @@ interface SettingsResponse { panel_title: string; /** Opt-in copy of the shipped Assist sentences into the config dir. */ install_assist_sentences?: boolean; + /** #146: household "low" floors (percent) for discovery + battery fleet. */ + default_consumable_threshold?: number; + battery_low_percent?: number; + /** Computed: what Battery Notes reports (null when not installed). */ + battery_notes?: { + default: number; + devices: number; + overrides: { name: string; device_id: string | null; threshold: number }[]; + more: number; + } | null; + /** #145: how task rows show Complete / Skip. */ + row_action_style?: "buttons_compact" | "buttons" | "icons"; + row_action_notice_pending?: boolean; }; notifications: { due_soon_enabled: boolean; @@ -241,6 +254,37 @@ export class MaintenanceSettingsView extends LitElement { this._vacWindowEnd = v.window_end; } + /** #146 follow-up: what Battery Notes currently reports, with the rule + * spelled out — warn-toned when its default sits above our floor (then a + * lowered floor would silently change nothing for noted batteries). */ + private _renderBatteryNotesHint(L: string) { + const bn = this._settings?.general?.battery_notes; + if (!bn || !bn.devices) return nothing; + const floor = this._settings?.general?.battery_low_percent ?? 20; + const warn = bn.default > floor; + const summary = t("bn_summary", L) + .replace("{name}", "Battery Notes") + .replace("{pct}", String(bn.default)) + .replace("{n}", String(bn.devices)); + const rule = (warn ? t("bn_above_floor", L).replace("{name}", "Battery Notes") : t("bn_floor_decides", L)) + .replace("{floor}", String(floor)); + return html` +
+ + + ${summary.split(":")[0]}${summary.slice(summary.indexOf(":"))} + ${bn.overrides.length + ? html` · ${t("bn_overrides", L).replace("{n}", String(bn.overrides.length + bn.more))} + ${bn.overrides.map((o, i) => html`${i ? " · " : " "}${o.device_id + ? html`${o.name}` + : o.name} (${o.threshold} %)`)} + ${bn.more ? html` ${t("bn_more", L).replace("{n}", String(bn.more))}` : nothing}` + : nothing} + ${rule} + +
`; + } + private async _updateSetting(key: string, value: unknown): Promise { try { const result = await this.hass.connection.sendMessagePromise({ @@ -594,6 +638,32 @@ export class MaintenanceSettingsView extends LitElement { if (v >= 0 && v <= 365) this._updateSetting("default_warning_days", v); }} /> + + + ${this._renderBatteryNotesHint(L)} +
${t("settings_thresholds_hint", L)}
+ + void) | null = null; @@ -158,6 +161,8 @@ export class MaintenanceSupporterCard extends LitElement { } private async _loadData(): Promise { + // #145: resolve the household's row-action style once (cached settings). + void getRowActionStyle(this.hass).then((s) => { this._globalRowStyle = s; }).catch(() => undefined); try { const [objResult, statsResult] = await Promise.all([ this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects", compact: true }), @@ -400,6 +405,9 @@ export class MaintenanceSupporterCard extends LitElement { const L = this._lang; const title = this._config.title || t("maintenance", L); const showHeader = this._config.show_header !== false; + // #145: the card's own action_style wins; otherwise follow the household + // setting (any "buttons*" style → labelled HA button, "icons" → icon). + const useButtons = (this._config.action_style ?? (this._globalRowStyle === "icons" ? "icons" : "buttons")) === "buttons"; const showActions = this._config.show_actions !== false; const compact = this._config.compact || false; const tasks = this._flatTasks; @@ -507,7 +515,38 @@ export class MaintenanceSupporterCard extends LitElement { ? "⚡" : "—"} - ${showActions + ${showActions && useButtons + ? html` + { + e.stopPropagation(); + const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog")!; + 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, + ); + }} + > + ${t("complete", L)} + + ` + : showActions ? html` = {}; @state() private _objects: MaintenanceObjectResponse[] = []; @@ -170,6 +176,10 @@ export class MaintenanceSupporterPanel extends LitElement { // value in the task-create dialog so the Settings → General → "Default // warning days" choice actually flows through to new tasks. @state() private _defaultWarningDays = 7; + /** #145: global "Task row actions" style (buttons_compact | buttons | icons). */ + @state() private _rowActionStyle = "buttons_compact"; + /** #145: one-time "rows look different now" notice for existing installs. */ + @state() private _rowActionNotice = false; @state() private _actionLoading = false; @state() private _moreMenuOpen = false; @state() private _objMenuOpen = false; @@ -320,6 +330,19 @@ export class MaintenanceSupporterPanel extends LitElement { else window.setTimeout(kick, 1500); window.addEventListener("popstate", this._popstateHandler); window.addEventListener("keydown", this._paletteKeydown); + if (typeof ResizeObserver !== "undefined") { + this._tightObserver = new ResizeObserver((entries) => { + const w = entries[0]?.contentRect.width ?? 0; + // 1000, not 880: between ~880 and ~1000 the wide grid technically + // fits its column minimums, but the chips track shrinks below any + // readable width — first the chips painted over the type column + // (duty-rotation GIF, 2026-08-30), clipped they left ellipsis + // fragments. Below ~1000 the narrow layout is the honest choice; + // from there up the chips track keeps ≥150 px of slack. + if (w > 0) this.tight = w < 1000; + }); + this._tightObserver.observe(this); + } window.addEventListener("resize", this._onVirtualScroll, { passive: true }); // localStorage.getItem can THROW (Safari private mode / locked-down policies // where storage access raises rather than returning null) — an unguarded @@ -360,6 +383,8 @@ export class MaintenanceSupporterPanel extends LitElement { super.disconnectedCallback(); window.removeEventListener("popstate", this._popstateHandler); window.removeEventListener("keydown", this._paletteKeydown); + this._tightObserver?.disconnect(); + this._tightObserver = null; window.removeEventListener("resize", this._onVirtualScroll); this.shadowRoot?.querySelector(".content")?.removeEventListener("scroll", this._onVirtualScroll); this._virtScrollAttached = false; @@ -517,7 +542,7 @@ export class MaintenanceSupporterPanel extends LitElement { features: AdvancedFeatures; admin_panel_user_ids?: string[]; operator_write_enabled?: boolean; - general?: { default_warning_days?: number }; + general?: { default_warning_days?: number; row_action_style?: string; row_action_notice_pending?: boolean }; objects_table_columns?: string[]; }; this._features = sr.features; @@ -527,6 +552,9 @@ export class MaintenanceSupporterPanel extends LitElement { if (typeof dwd === "number" && dwd >= 0 && dwd <= 365) { this._defaultWarningDays = dwd; } + const ras = sr.general?.row_action_style; + this._rowActionStyle = ras === "icons" || ras === "buttons" ? ras : "buttons_compact"; + this._rowActionNotice = sr.general?.row_action_notice_pending === true; this._objectsTableColumns = sanitizeColumns(sr.objects_table_columns); } @@ -1995,6 +2023,20 @@ export class MaintenanceSupporterPanel extends LitElement { ` : nothing} + ${this._rowActionNotice && !!this.hass?.user?.is_admin + ? html` +
+ + ${t("row_actions_banner", this._lang)} + this._dismissRowActionNotice(false)}> + ${t("row_actions_keep", this._lang)} + + this._dismissRowActionNotice(true)}> + ${t("row_actions_back", this._lang)} + +
+ ` + : nothing} ${this.narrow || this._view !== "overview" ? this._renderHeader() : nothing}
${this._view === "overview" @@ -2285,10 +2327,19 @@ export class MaintenanceSupporterPanel extends LitElement {
${row.task_name}
${row.object_name} · ${formatDueDays(row.days_until_due, L)}
- { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> - - + ${this._actionStyle() === "icons" + ? html` + { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> + + ` + : html` + { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> + ${this._actionStyle() === "buttons_compact" && (this.narrow || this.tight) + ? html`` + : html`${t("complete", L)}`} + `} `)} @@ -3215,18 +3266,74 @@ export class MaintenanceSupporterPanel extends LitElement { : nothing} ${renderMiniSparkline(row, this._miniStatsData, this._lang)} - - { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> - - - { e.stopPropagation(); this._promptSkipTask(row.entry_id, row.task_id); }}> - - - + ${this._renderRowActions(L, () => this._openCompleteDialogForRow(row), () => this._promptSkipTask(row.entry_id, row.task_id))} `; } + /** DESIGN PROTOTYPE (#145 wish 1): row actions as bare icons (current) or + * as labelled colour-coded buttons. Switch: localStorage "msp-action-style" + * = "buttons" — evaluation only, no settings plumbing yet. */ + private _actionStyle(): string { + return this._rowActionStyle; + } + + /** The #145 banner's two ways out: keep the buttons (just dismiss) or go + * back to icons (style + dismiss in one write). Admin-only, like every + * global setting. */ + private async _dismissRowActionNotice(backToIcons: boolean): Promise { + const settings: Record = { row_action_notice_pending: false }; + if (backToIcons) settings.row_action_style = "icons"; + try { + await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/global/update", settings }); + this._rowActionNotice = false; + if (backToIcons) this._rowActionStyle = "icons"; + } catch (err) { + console.warn("[maintenance-supporter] row-action notice update failed", err); + } + } + + /** #145: row actions as HA buttons — labelled (accent/success + plain, the + * task-detail pairing) or, for "buttons_compact" on narrow screens, + * icon-only — or the classic icon pair when the household chose "icons". */ + private _renderRowActions(L: string, onComplete: () => void, onSkip: () => void): TemplateResult { + const style = this._actionStyle(); + if (style === "buttons" || style === "buttons_compact") { + const iconOnly = style === "buttons_compact" && (this.narrow || this.tight); + if (iconOnly) { + // Compact narrow form: the primary action stays a solid ha-button + // (icon only), the secondary one becomes HA's own icon button. + return html` + + { e.stopPropagation(); onComplete(); }}> + + + { e.stopPropagation(); onSkip(); }}> + + + `; + } + return html` + + { e.stopPropagation(); onComplete(); }}> + ${t("complete", L)} + + { e.stopPropagation(); onSkip(); }}> + ${t("skip", L)} + + `; + } + return html` + + { e.stopPropagation(); onComplete(); }}> + + + { e.stopPropagation(); onSkip(); }}> + + + `; + } + private _openCompleteDialogForRow(row: TaskRow): void { const obj = this._objects.find(o => o.entry_id === row.entry_id); const task = obj?.tasks.find(t => t.id === row.task_id); @@ -3367,14 +3474,11 @@ export class MaintenanceSupporterPanel extends LitElement { : nothing} ${renderMiniSparkline(task, this._miniStatsData, this._lang)} - - { e.stopPropagation(); this._openCompleteDialog(obj.entry_id, task.id, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled); }}> - - - { e.stopPropagation(); this._promptSkipTask(obj.entry_id, task.id); }}> - - - + ${this._renderRowActions( + L, + () => this._openCompleteDialog(obj.entry_id, task.id, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled), + () => this._promptSkipTask(obj.entry_id, task.id), + )} `)}`} diff --git a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts index d2dfffda..d31984ec 100644 --- a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts +++ b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts @@ -287,6 +287,10 @@ export const panelStyles = css` .today-task { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .today-object { color: var(--secondary-text-color); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .today-row .btn-complete { color: var(--success-color, #4caf50); flex: none; } + .today-row .today-complete { flex: none; --ha-button-font-size: 13px; white-space: nowrap; } + .today-row .today-complete ha-icon { --mdc-icon-size: 18px; } + :host([narrow]) .today-row .today-complete, + :host([tight]) .today-row .today-complete { min-width: 0; --ha-button-height: 36px; } .today-empty { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 48px 16px; color: var(--secondary-text-color); text-align: center; @@ -376,6 +380,17 @@ export const panelStyles = css` color: var(--secondary-text-color); flex-wrap: wrap; justify-content: flex-end; + /* The chips track is minmax(0, …): when the row gets squeezed the track + may shrink below the chips' natural width — clip instead of painting + over the neighbouring type column (duty-rotation GIF, 2026-08-30). */ + min-width: 0; + overflow: hidden; + /* …and all-or-nothing: below 60 px of track there is no readable chip, + only fragments — hide them entirely via the container query below. */ + container-type: inline-size; + } + @container (max-width: 60px) { + .sub-chip { display: none; } } /* Empty subline still occupies its grid slot so neighbouring columns line up */ .task-sub-empty { min-height: 1px; } @@ -388,6 +403,11 @@ export const panelStyles = css` border-radius: 10px; background: var(--secondary-background-color, rgba(127, 127, 127, 0.1)); line-height: 1.4; + /* A single chip wider than the squeezed track truncates instead of + bleeding into the next column. */ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; } .sub-chip ha-icon { --mdc-icon-size: 14px; @@ -407,6 +427,16 @@ export const panelStyles = css` --mdc-icon-size: 26px; } + /* DESIGN PROTOTYPE (#145 wish 1): labelled, colour-coded action buttons + instead of bare icons. Complete = filled success pill, Skip = outlined + neutral pill; both keep the icon as a leading glyph. */ + .row-actions.as-buttons { gap: 4px; } + .row-actions.as-buttons ha-button { + --ha-button-font-size: 13px; + white-space: nowrap; + } + .row-actions.as-buttons ha-icon { --mdc-icon-size: 18px; } + /* Custom elements default to display:inline; the task-detail component renders light-DOM and must behave like the block it wraps. */ maintenance-task-detail-view { display: block; } @@ -1440,7 +1470,7 @@ export const panelStyles = css` full bottom row. */ display: grid; grid-column: auto; - grid-template-columns: auto minmax(0, 1fr) 100px auto; + grid-template-columns: auto minmax(76px, 1fr) fit-content(100px) auto; /* name column keeps a word-sized floor, the due column hugs its content — at 400 px the object name used to be clipped mid-word (2026-08-30) */ grid-template-rows: auto auto auto; column-gap: 8px; row-gap: 4px; @@ -1480,6 +1510,56 @@ export const panelStyles = css` grid-column: 4; grid-row: 2; } + /* Prototype: on phones the two labelled pills stack, so the row keeps its + deterministic column X-positions and neither label gets truncated. */ + /* Labelled buttons stack when the row cannot hold them side by side: + narrow viewports AND a tight panel (iPad portrait with the sidebar + docked — the viewport is not narrow but the panel is ~768 px). */ + :host([narrow]) .row-actions.as-buttons, + :host([tight]) .row-actions.as-buttons { + flex-direction: column; + align-items: stretch; + gap: 4px; + } + :host([narrow]) .row-actions.as-buttons ha-button, + :host([tight]) .row-actions.as-buttons ha-button { --ha-button-font-size: 12px; } + /* buttons_compact: the labelled buttons collapse to an icon-only ha-button + plus ha-icon-button side by side — the colour carries the meaning, the + row keeps the icon-era height. */ + :host([narrow]) .row-actions.as-buttons.compact, + :host([tight]) .row-actions.as-buttons.compact { flex-direction: row; align-items: center; gap: 2px; } + :host([narrow]) .row-actions.compact ha-button, + :host([tight]) .row-actions.compact ha-button { --ha-button-height: 36px; min-width: 0; } + :host([narrow]) .row-actions.compact ha-icon-button, + :host([tight]) .row-actions.compact ha-icon-button { --mdc-icon-button-size: 40px; --mdc-icon-size: 22px; color: var(--secondary-text-color); } + :host([narrow]) .row-actions.compact ha-icon, + :host([tight]) .row-actions.compact ha-icon { --mdc-icon-size: 20px; } + + /* A TIGHT panel that is not narrow (iPad portrait with HA's sidebar + docked: 1024 px viewport, ~768 px panel) gets the narrow row layout too — + the wide subgrid overflowed its last column at that width even before + the buttons. Same rules as the :host([narrow]) block above / the + max-width:768px media mirror below, keyed on the panel's own width. */ + :host([tight]:not([narrow])) .task-table { display: block; } + :host([tight]:not([narrow])) .task-row { + display: grid; + grid-column: auto; + grid-template-columns: auto minmax(76px, 1fr) fit-content(100px) auto; /* name column keeps a word-sized floor, the due column hugs its content — at 400 px the object name used to be clipped mid-word (2026-08-30) */ + grid-template-rows: auto auto auto; + column-gap: 8px; + row-gap: 4px; + padding: 12px; + min-width: 0; + } + :host([tight]:not([narrow])) .cell.type { display: none; } + :host([tight]:not([narrow])) .cell.task-name { grid-column: 1 / -1; grid-row: 1; min-width: 0; } + :host([tight]:not([narrow])) .cell-badges { grid-column: 1; grid-row: 2; } + :host([tight]:not([narrow])) .cell.object-name { grid-column: 2; grid-row: 2; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.2; } + :host([tight]:not([narrow])) .due-cell { grid-column: 3; grid-row: 2; align-items: flex-end; min-width: 0; } + :host([tight]:not([narrow])) .row-actions { grid-column: 4; grid-row: 2; } + :host([tight]:not([narrow])) .task-sub { grid-column: 1 / -1; grid-row: 3; font-size: 11px; gap: 6px; justify-content: flex-start; flex-wrap: wrap; } + :host([tight]:not([narrow])) .task-sub-empty { display: none; } + :host([tight]:not([narrow])) .mini-sparkline { width: 50px; } :host([narrow]) .task-sub { grid-column: 1 / -1; grid-row: 3; @@ -1614,7 +1694,7 @@ export const panelStyles = css` /* Mirror the :host([narrow]) grid layout for narrow desktop windows */ .task-row { display: grid; - grid-template-columns: auto minmax(0, 1fr) 100px auto; + grid-template-columns: auto minmax(76px, 1fr) fit-content(100px) auto; /* name column keeps a word-sized floor, the due column hugs its content — at 400 px the object name used to be clipped mid-word (2026-08-30) */ grid-template-rows: auto auto auto; column-gap: 8px; row-gap: 4px; diff --git a/custom_components/maintenance_supporter/frontend-src/types.ts b/custom_components/maintenance_supporter/frontend-src/types.ts index 1b817de9..90f277f9 100644 --- a/custom_components/maintenance_supporter/frontend-src/types.ts +++ b/custom_components/maintenance_supporter/frontend-src/types.ts @@ -373,6 +373,9 @@ export interface SavedView { export interface CardConfig { type: string; title?: string; + /** #145: how the row's Complete action looks. Omit to follow the global + * "Task row actions" setting (buttons unless the household chose icons). */ + action_style?: "icons" | "buttons"; show_header?: boolean; max_items?: number; filter_status?: string[]; diff --git a/custom_components/maintenance_supporter/frontend/locales/cs.json b/custom_components/maintenance_supporter/frontend/locales/cs.json index 849e7f6c..37ae4ba0 100644 --- a/custom_components/maintenance_supporter/frontend/locales/cs.json +++ b/custom_components/maintenance_supporter/frontend/locales/cs.json @@ -374,6 +374,7 @@ "card_title": "Název", "card_show_header": "Zobrazit záhlaví se statistikami", "card_show_actions": "Zobrazit tlačítka akcí", + "card_action_style": "Styl tlačítka Dokončit", "card_compact": "Kompaktní režim", "card_max_items": "Max položek (0 = vše)", "card_filter_status": "Filtrovat podle stavu", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Vícestupňové procedury pro dokončení úkolu", "settings_general": "Obecné", "settings_default_warning": "Výchozí dny upozornění", + "settings_consumable_threshold": "Práh „spotřební materiál nízký“ (%)", + "settings_battery_low_percent": "Práh „baterie slabá“ (%)", + "settings_thresholds_hint": "Výchozí hodnoty pro nová Navrhovaná nastavení (zbývající procenta) a pro baterie flotily bez vlastního prahu Battery Notes. Stávající úkoly si ponechají svou hodnotu.", + "bn_summary": "{name}: {pct} % pro {n} baterií", + "bn_floor_decides": "Tvůj práh {floor} % rozhoduje pro všechny; u každé baterie platí vyšší z obou prahů.", + "bn_above_floor": "To je nad tvým prahem {floor} %, takže o všech poznamenaných bateriích rozhoduje {name}.", + "bn_overrides": "{n} zařízení s vlastním prahem:", + "bn_more": "+ {n} dalších", + "settings_row_actions": "Akce v řádcích úkolů", + "row_actions_buttons_compact": "Tlačítka (na telefonu jen ikony)", + "row_actions_buttons": "Tlačítka s textem", + "row_actions_icons": "Ikony (klasické)", + "row_actions_follow": "Podle nastavení domácnosti", "settings_panel_enabled": "Boční panel", "settings_panel_title": "Název panelu", "settings_notifications": "Oznámení", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.", "update_banner": "Na serveru je novější verze Maintenance Supporter — načtěte znovu pro aktualizaci panelu.", "update_reload": "Načíst znovu", + "row_actions_banner": "Dokončit a Přeskočit jsou v řádcích úkolů nyní tlačítka. Dáváte přednost starým ikonám?", + "row_actions_keep": "Ponechat tlačítka", + "row_actions_back": "Zpět na ikony", "battery_fleet_forecast_overdue": "Předpovězené datum uplynulo — baterie stále hlásí dobrý stav. Pokud jste ji vyměnili, zaznamenejte výměnu; jinak byla předpověď mylná.", "cost_from_parts": "Použít ≈ {amount} z dílů", "dismiss": "Skrýt", diff --git a/custom_components/maintenance_supporter/frontend/locales/da.json b/custom_components/maintenance_supporter/frontend/locales/da.json index e16b35fc..08522fd4 100644 --- a/custom_components/maintenance_supporter/frontend/locales/da.json +++ b/custom_components/maintenance_supporter/frontend/locales/da.json @@ -376,6 +376,7 @@ "card_title": "Titel", "card_show_header": "Vis overskrift med statistik", "card_show_actions": "Vis handlingsknapper", + "card_action_style": "Stil for Fuldfør-knappen", "card_compact": "Kompakt tilstand", "card_max_items": "Maks. elementer (0 = alle)", "card_filter_status": "Filtrer efter status", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Procedurer i flere trin til fuldførelse af opgaver", "settings_general": "Generelt", "settings_default_warning": "Standard advarselsdage", + "settings_consumable_threshold": "Tærskel „forbrugsvare lav“ (%)", + "settings_battery_low_percent": "Tærskel „batteri lavt“ (%)", + "settings_thresholds_hint": "Standardværdier for nye Foreslåede opsætninger (resterende procent) og for flådebatterier uden egen Battery Notes-tærskel. Eksisterende opgaver beholder deres værdi.", + "bn_summary": "{name}: {pct} % for {n} batterier", + "bn_floor_decides": "Din tærskel på {floor} % afgør for alle; pr. batteri gælder den højeste af de to tærskler.", + "bn_above_floor": "Det er over din tærskel på {floor} %, så {name} afgør for alle noterede batterier.", + "bn_overrides": "{n} enheder med egen tærskel:", + "bn_more": "+ {n} mere", + "settings_row_actions": "Handlinger i opgaverækker", + "row_actions_buttons_compact": "Knapper (kun ikoner på telefonen)", + "row_actions_buttons": "Knapper med tekst", + "row_actions_icons": "Ikoner (klassisk)", + "row_actions_follow": "Følg husstandens indstilling", "settings_panel_enabled": "Sidepanel", "settings_panel_title": "Sidepanelets titel", "settings_notifications": "Notifikationer", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.", "update_banner": "En nyere version af Maintenance Supporter er på serveren — genindlæs for at opdatere panelet.", "update_reload": "Genindlæs", + "row_actions_banner": "Fuldfør og Spring over er nu knapper i opgaverækkerne. Foretrækker du de gamle ikoner?", + "row_actions_keep": "Behold knapper", + "row_actions_back": "Tilbage til ikoner", "battery_fleet_forecast_overdue": "Forudsagt dato er overskredet — batteriet melder stadig god tilstand. Hvis du har skiftet det, registrér udskiftningen; ellers ramte prognosen forbi.", "cost_from_parts": "Brug ≈ {amount} fra dele", "dismiss": "Afvis", diff --git a/custom_components/maintenance_supporter/frontend/locales/de.json b/custom_components/maintenance_supporter/frontend/locales/de.json index 60530434..65f6b5ed 100644 --- a/custom_components/maintenance_supporter/frontend/locales/de.json +++ b/custom_components/maintenance_supporter/frontend/locales/de.json @@ -376,6 +376,7 @@ "card_title": "Titel", "card_show_header": "Kopfzeile mit Statistiken anzeigen", "card_show_actions": "Aktionsbuttons anzeigen", + "card_action_style": "Stil des Erledigt-Buttons", "card_compact": "Kompaktmodus", "card_max_items": "Max. Einträge (0 = alle)", "card_filter_status": "Nach Status filtern", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Mehrstufige Verfahren zur Aufgabenerlediung", "settings_general": "Allgemein", "settings_default_warning": "Standard-Warntage", + "settings_consumable_threshold": "Schwelle „Verbrauchsmaterial niedrig“ (%)", + "settings_battery_low_percent": "Schwelle „Batterie niedrig“ (%)", + "settings_thresholds_hint": "Vorgaben für neue Vorgeschlagene Einrichtungen (Prozent-Restwert) und für Flotten-Batterien ohne eigene Battery-Notes-Schwelle. Bestehende Aufgaben behalten ihren Wert.", + "bn_summary": "{name}: {pct} % für {n} Batterien", + "bn_floor_decides": "Deine {floor}-%-Schwelle entscheidet für alle; pro Batterie zählt die höhere der beiden Schwellen.", + "bn_above_floor": "Das liegt über deiner {floor}-%-Schwelle — für alle notierten Batterien entscheidet {name}.", + "bn_overrides": "{n} Geräte mit eigener Schwelle:", + "bn_more": "+ {n} weitere", + "settings_row_actions": "Aktionen in Aufgabenzeilen", + "row_actions_buttons_compact": "Buttons (auf dem Handy nur Symbole)", + "row_actions_buttons": "Buttons mit Text", + "row_actions_icons": "Symbole (klassisch)", + "row_actions_follow": "Wie in den Einstellungen", "settings_panel_enabled": "Seitenleisten-Panel", "settings_panel_title": "Panel-Titel", "settings_notifications": "Benachrichtigungen", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.", "update_banner": "Auf dem Server läuft eine neuere Version von Maintenance Supporter — neu laden, um das Panel zu aktualisieren.", "update_reload": "Neu laden", + "row_actions_banner": "Erledigt und Überspringen sind in den Aufgabenzeilen jetzt Buttons. Lieber die alten Symbole?", + "row_actions_keep": "Buttons behalten", + "row_actions_back": "Zurück zu Symbolen", "battery_fleet_forecast_overdue": "Prognosedatum überschritten — die Batterie meldet sich weiterhin gesund. Falls du sie gewechselt hast, trage den Wechsel nach; andernfalls lag die Prognose daneben.", "cost_from_parts": "≈ {amount} aus Teilen übernehmen", "dismiss": "Ausblenden", diff --git a/custom_components/maintenance_supporter/frontend/locales/en.json b/custom_components/maintenance_supporter/frontend/locales/en.json index 285de2da..6832e2b7 100644 --- a/custom_components/maintenance_supporter/frontend/locales/en.json +++ b/custom_components/maintenance_supporter/frontend/locales/en.json @@ -376,6 +376,7 @@ "card_title": "Title", "card_show_header": "Show header with statistics", "card_show_actions": "Show action buttons", + "card_action_style": "Complete button style", "card_compact": "Compact mode", "card_max_items": "Max items (0 = all)", "card_filter_status": "Filter by status", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Multi-step procedures for task completion", "settings_general": "General", "settings_default_warning": "Default warning days", + "settings_consumable_threshold": "Consumable low threshold (%)", + "settings_battery_low_percent": "Battery low threshold (%)", + "settings_thresholds_hint": "Defaults for new Suggested setups (percent-remaining consumables) and for fleet batteries without their own Battery Notes threshold. Existing tasks keep their value.", + "bn_summary": "{name}: {pct} % for {n} batteries", + "bn_floor_decides": "Your {floor} % floor decides for all of them; the higher of the two thresholds counts per battery.", + "bn_above_floor": "That is above your {floor} % floor, so {name} decides for all noted batteries.", + "bn_overrides": "{n} devices with their own threshold:", + "bn_more": "+ {n} more", + "settings_row_actions": "Task row actions", + "row_actions_buttons_compact": "Buttons (icons only on phones)", + "row_actions_buttons": "Buttons with text", + "row_actions_icons": "Icons (classic)", + "row_actions_follow": "Follow the household setting", "settings_panel_enabled": "Sidebar panel", "settings_panel_title": "Sidebar panel title", "settings_notifications": "Notifications", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.", "update_banner": "A newer version of Maintenance Supporter is on the server — reload to update the panel.", "update_reload": "Reload", + "row_actions_banner": "Complete and Skip in task rows are now buttons. Prefer the old icons?", + "row_actions_keep": "Keep buttons", + "row_actions_back": "Back to icons", "battery_fleet_forecast_overdue": "Predicted date passed — the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.", "cost_from_parts": "Use ≈ {amount} from parts", "dismiss": "Dismiss", diff --git a/custom_components/maintenance_supporter/frontend/locales/es.json b/custom_components/maintenance_supporter/frontend/locales/es.json index 5ef0d8b0..7dff280d 100644 --- a/custom_components/maintenance_supporter/frontend/locales/es.json +++ b/custom_components/maintenance_supporter/frontend/locales/es.json @@ -374,6 +374,7 @@ "card_title": "Título", "card_show_header": "Mostrar encabezado con estadísticas", "card_show_actions": "Mostrar botones de acción", + "card_action_style": "Estilo del botón Completar", "card_compact": "Modo compacto", "card_max_items": "Máx. elementos (0 = todos)", "card_filter_status": "Filtrar por estado", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Procedimientos de varios pasos para completar tareas", "settings_general": "General", "settings_default_warning": "Días de aviso predeterminados", + "settings_consumable_threshold": "Umbral „consumible bajo“ (%)", + "settings_battery_low_percent": "Umbral „pila baja“ (%)", + "settings_thresholds_hint": "Valores predeterminados para nuevas Configuraciones sugeridas (porcentaje restante) y para pilas de la flota sin umbral propio de Battery Notes. Las tareas existentes conservan su valor.", + "bn_summary": "{name}: {pct} % para {n} pilas", + "bn_floor_decides": "Tu umbral del {floor} % decide para todas; por pila cuenta el mayor de los dos umbrales.", + "bn_above_floor": "Está por encima de tu umbral del {floor} %, así que {name} decide para todas las pilas anotadas.", + "bn_overrides": "{n} dispositivos con umbral propio:", + "bn_more": "+ {n} más", + "settings_row_actions": "Acciones en las filas de tareas", + "row_actions_buttons_compact": "Botones (solo iconos en el móvil)", + "row_actions_buttons": "Botones con texto", + "row_actions_icons": "Iconos (clásico)", + "row_actions_follow": "Seguir el ajuste del hogar", "settings_panel_enabled": "Panel lateral", "settings_panel_title": "Título del panel lateral", "settings_notifications": "Notificaciones", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.", "update_banner": "Hay una versión más reciente de Maintenance Supporter en el servidor — recarga para actualizar el panel.", "update_reload": "Recargar", + "row_actions_banner": "Completar y Omitir ahora son botones en las filas de tareas. ¿Prefieres los iconos antiguos?", + "row_actions_keep": "Mantener botones", + "row_actions_back": "Volver a los iconos", "battery_fleet_forecast_overdue": "Fecha prevista superada: la batería sigue informando buen estado. Si la cambiaste, registra el reemplazo; si no, la previsión falló.", "cost_from_parts": "Usar ≈ {amount} de las piezas", "dismiss": "Descartar", diff --git a/custom_components/maintenance_supporter/frontend/locales/fi.json b/custom_components/maintenance_supporter/frontend/locales/fi.json index 0546da84..94e36e90 100644 --- a/custom_components/maintenance_supporter/frontend/locales/fi.json +++ b/custom_components/maintenance_supporter/frontend/locales/fi.json @@ -376,6 +376,7 @@ "card_title": "Otsikko", "card_show_header": "Näytä otsikko tilastoineen", "card_show_actions": "Näytä toimintopainikkeet", + "card_action_style": "Valmis-painikkeen tyyli", "card_compact": "Tiivis tila", "card_max_items": "Enimmäismäärä (0 = kaikki)", "card_filter_status": "Suodata tilan mukaan", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Monivaiheiset menettelyt tehtävän suorittamiseen", "settings_general": "Yleiset", "settings_default_warning": "Oletusvaroituspäivät", + "settings_consumable_threshold": "Kynnys „kulutustarvike vähissä“ (%)", + "settings_battery_low_percent": "Kynnys „paristo vähissä“ (%)", + "settings_thresholds_hint": "Oletukset uusille Ehdotetuille asetuksille (jäljellä oleva prosentti) ja paristokannan paristoille ilman omaa Battery Notes -kynnystä. Olemassa olevat tehtävät säilyttävät arvonsa.", + "bn_summary": "{name}: {pct} % {n} paristolle", + "bn_floor_decides": "Sinun {floor} %:n kynnyksesi ratkaisee kaikille; paristoa kohden pätee korkeampi kahdesta kynnyksestä.", + "bn_above_floor": "Se on sinun {floor} %:n kynnyksesi yläpuolella, joten {name} ratkaisee kaikille kirjatuille paristoille.", + "bn_overrides": "{n} laitetta, joilla on oma kynnys:", + "bn_more": "+ {n} lisää", + "settings_row_actions": "Tehtävärivien toiminnot", + "row_actions_buttons_compact": "Painikkeet (puhelimessa vain kuvakkeet)", + "row_actions_buttons": "Painikkeet tekstillä", + "row_actions_icons": "Kuvakkeet (perinteinen)", + "row_actions_follow": "Kotitalouden asetuksen mukaan", "settings_panel_enabled": "Sivupalkin paneeli", "settings_panel_title": "Sivupalkin paneelin otsikko", "settings_notifications": "Ilmoitukset", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.", "update_banner": "Palvelimella on uudempi Maintenance Supporter -versio — lataa uudelleen päivittääksesi paneelin.", "update_reload": "Lataa uudelleen", + "row_actions_banner": "Valmis ja Ohita ovat tehtäväriveillä nyt painikkeita. Haluatko mieluummin vanhat kuvakkeet?", + "row_actions_keep": "Pidä painikkeet", + "row_actions_back": "Takaisin kuvakkeisiin", "battery_fleet_forecast_overdue": "Ennustettu päivä on ohitettu — akku ilmoittaa yhä hyvästä kunnosta. Jos vaihdoit sen, kirjaa vaihto; muuten ennuste oli pielessä.", "cost_from_parts": "Käytä ≈ {amount} osista", "dismiss": "Hylkää", diff --git a/custom_components/maintenance_supporter/frontend/locales/fr.json b/custom_components/maintenance_supporter/frontend/locales/fr.json index 89e21d39..961f9ce5 100644 --- a/custom_components/maintenance_supporter/frontend/locales/fr.json +++ b/custom_components/maintenance_supporter/frontend/locales/fr.json @@ -374,6 +374,7 @@ "card_title": "Titre", "card_show_header": "Afficher l'en-tête avec statistiques", "card_show_actions": "Afficher les boutons d'action", + "card_action_style": "Style du bouton Terminer", "card_compact": "Mode compact", "card_max_items": "Nombre max (0 = tous)", "card_filter_status": "Filtrer par statut", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Procédures multi-étapes pour la réalisation des tâches", "settings_general": "Général", "settings_default_warning": "Jours d'avertissement par défaut", + "settings_consumable_threshold": "Seuil « consommable bas » (%)", + "settings_battery_low_percent": "Seuil « pile faible » (%)", + "settings_thresholds_hint": "Valeurs par défaut pour les nouvelles Configurations suggérées (pourcentage restant) et pour les piles de la flotte sans seuil Battery Notes propre. Les tâches existantes gardent leur valeur.", + "bn_summary": "{name} : {pct} % pour {n} piles", + "bn_floor_decides": "Votre seuil de {floor} % décide pour toutes ; par pile, c'est le plus élevé des deux seuils qui compte.", + "bn_above_floor": "C'est au-dessus de votre seuil de {floor} % : {name} décide donc pour toutes les piles notées.", + "bn_overrides": "{n} appareils avec leur propre seuil :", + "bn_more": "+ {n} autres", + "settings_row_actions": "Actions des lignes de tâches", + "row_actions_buttons_compact": "Boutons (icônes seules sur téléphone)", + "row_actions_buttons": "Boutons avec texte", + "row_actions_icons": "Icônes (classique)", + "row_actions_follow": "Suivre le réglage du foyer", "settings_panel_enabled": "Panneau latéral", "settings_panel_title": "Titre du panneau latéral", "settings_notifications": "Notifications", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.", "update_banner": "Une version plus récente de Maintenance Supporter est sur le serveur — rechargez pour mettre à jour le panneau.", "update_reload": "Recharger", + "row_actions_banner": "Terminer et Ignorer sont désormais des boutons dans les lignes de tâches. Vous préférez les anciennes icônes ?", + "row_actions_keep": "Garder les boutons", + "row_actions_back": "Revenir aux icônes", "battery_fleet_forecast_overdue": "Date prévue dépassée — la batterie se signale toujours en bon état. Si vous l'avez remplacée, enregistrez le remplacement ; sinon la prévision était erronée.", "cost_from_parts": "Reprendre ≈ {amount} des pièces", "dismiss": "Ignorer", diff --git a/custom_components/maintenance_supporter/frontend/locales/hi.json b/custom_components/maintenance_supporter/frontend/locales/hi.json index 51c29775..9c429624 100644 --- a/custom_components/maintenance_supporter/frontend/locales/hi.json +++ b/custom_components/maintenance_supporter/frontend/locales/hi.json @@ -376,6 +376,7 @@ "card_title": "शीर्षक", "card_show_header": "सांख्यिकी के साथ हेडर दिखाएँ", "card_show_actions": "क्रिया बटन दिखाएँ", + "card_action_style": "पूर्ण बटन की शैली", "card_compact": "संक्षिप्त मोड", "card_max_items": "अधिकतम आइटम (0 = सभी)", "card_filter_status": "स्थिति के अनुसार फ़िल्टर करें", @@ -450,6 +451,19 @@ "feat_checklists_desc": "कार्य पूर्णता के लिए बहु-चरणीय प्रक्रियाएँ", "settings_general": "सामान्य", "settings_default_warning": "डिफ़ॉल्ट चेतावनी दिन", + "settings_consumable_threshold": "उपभोग्य „कम“ सीमा (%)", + "settings_battery_low_percent": "बैटरी „कम“ सीमा (%)", + "settings_thresholds_hint": "नए सुझाए गए सेटअप (शेष प्रतिशत) और अपनी Battery Notes सीमा न रखने वाली बेड़े की बैटरियों के लिए डिफ़ॉल्ट। मौजूदा कार्य अपना मान रखते हैं।", + "bn_summary": "{name}: {n} बैटरियों के लिए {pct} %", + "bn_floor_decides": "आपकी {floor} % सीमा सभी के लिए निर्णायक है; प्रत्येक बैटरी के लिए दोनों में से ऊँची सीमा मान्य है।", + "bn_above_floor": "यह आपकी {floor} % सीमा से ऊपर है, इसलिए सभी नोट की गई बैटरियों के लिए {name} निर्णायक है।", + "bn_overrides": "{n} डिवाइस अपनी सीमा के साथ:", + "bn_more": "+ {n} और", + "settings_row_actions": "कार्य पंक्ति क्रियाएँ", + "row_actions_buttons_compact": "बटन (फ़ोन पर केवल आइकन)", + "row_actions_buttons": "टेक्स्ट वाले बटन", + "row_actions_icons": "आइकन (क्लासिक)", + "row_actions_follow": "घर की सेटिंग के अनुसार", "settings_panel_enabled": "साइडबार पैनल", "settings_panel_title": "साइडबार पैनल शीर्षक", "settings_notifications": "सूचनाएँ", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।", "update_banner": "सर्वर पर Maintenance Supporter का नया संस्करण है — पैनल अपडेट करने के लिए पुनः लोड करें।", "update_reload": "पुनः लोड करें", + "row_actions_banner": "कार्य पंक्तियों में पूर्ण और छोड़ें अब बटन हैं। पुराने आइकन पसंद हैं?", + "row_actions_keep": "बटन रखें", + "row_actions_back": "आइकन पर वापस जाएँ", "battery_fleet_forecast_overdue": "अनुमानित तिथि बीत गई — बैटरी अब भी अच्छी स्थिति बता रही है। यदि आपने इसे बदला है, तो बदलाव दर्ज करें; अन्यथा पूर्वानुमान गलत था।", "cost_from_parts": "पुर्ज़ों से ≈ {amount} लें", "dismiss": "हटाएँ", diff --git a/custom_components/maintenance_supporter/frontend/locales/hu.json b/custom_components/maintenance_supporter/frontend/locales/hu.json index 28ffd1eb..0107af75 100644 --- a/custom_components/maintenance_supporter/frontend/locales/hu.json +++ b/custom_components/maintenance_supporter/frontend/locales/hu.json @@ -376,6 +376,7 @@ "card_title": "Cím", "card_show_header": "Fejléc megjelenítése statisztikákkal", "card_show_actions": "Műveletgombok megjelenítése", + "card_action_style": "A Kész gomb stílusa", "card_compact": "Kompakt mód", "card_max_items": "Max. elemszám (0 = mind)", "card_filter_status": "Szűrés állapot szerint", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Többlépéses eljárások a feladatok elvégzéséhez", "settings_general": "Általános", "settings_default_warning": "Alapértelmezett figyelmeztetési napok", + "settings_consumable_threshold": "„Fogyóeszköz alacsony” küszöb (%)", + "settings_battery_low_percent": "„Alacsony akkumulátor” küszöb (%)", + "settings_thresholds_hint": "Alapértékek az új Javasolt beállításokhoz (százalékos maradék) és a saját Battery Notes-küszöb nélküli flottaelemekhez. A meglévő feladatok megtartják az értéküket.", + "bn_summary": "{name}: {pct} % {n} elemhez", + "bn_floor_decides": "A te {floor} %-os küszöböd dönt mindegyiknél; elemenként a két küszöb közül a magasabb számít.", + "bn_above_floor": "Ez a te {floor} %-os küszöböd felett van, így minden feljegyzett elemnél a {name} dönt.", + "bn_overrides": "{n} eszköz saját küszöbbel:", + "bn_more": "+ még {n}", + "settings_row_actions": "Műveletek a feladatsorokban", + "row_actions_buttons_compact": "Gombok (telefonon csak ikonok)", + "row_actions_buttons": "Gombok szöveggel", + "row_actions_icons": "Ikonok (klasszikus)", + "row_actions_follow": "A háztartás beállítása szerint", "settings_panel_enabled": "Oldalsáv-panel", "settings_panel_title": "Oldalsáv-panel címe", "settings_notifications": "Értesítések", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.", "update_banner": "A Maintenance Supporter újabb verziója érhető el a szerveren — töltse újra az oldalt a panel frissítéséhez.", "update_reload": "Újratöltés", + "row_actions_banner": "A Kész és a Kihagyás a feladatsorokban mostantól gombok. Inkább a régi ikonokat szeretné?", + "row_actions_keep": "Gombok megtartása", + "row_actions_back": "Vissza az ikonokhoz", "battery_fleet_forecast_overdue": "Az előrejelzett dátum elmúlt — az elem továbbra is jó állapotot jelez. Ha kicserélted, rögzítsd a cserét; különben az előrejelzés tévedett.", "cost_from_parts": "≈ {amount} átvétele az alkatrészekből", "dismiss": "Elrejtés", diff --git a/custom_components/maintenance_supporter/frontend/locales/it.json b/custom_components/maintenance_supporter/frontend/locales/it.json index 52c6ec1a..66cf4a74 100644 --- a/custom_components/maintenance_supporter/frontend/locales/it.json +++ b/custom_components/maintenance_supporter/frontend/locales/it.json @@ -374,6 +374,7 @@ "card_title": "Titolo", "card_show_header": "Mostra intestazione con statistiche", "card_show_actions": "Mostra pulsanti azione", + "card_action_style": "Stile del pulsante Completa", "card_compact": "Modalità compatta", "card_max_items": "Max elementi (0 = tutti)", "card_filter_status": "Filtra per stato", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Procedure multi-fase per il completamento delle attività", "settings_general": "Generale", "settings_default_warning": "Giorni di avviso predefiniti", + "settings_consumable_threshold": "Soglia „consumabile basso“ (%)", + "settings_battery_low_percent": "Soglia „batteria scarica“ (%)", + "settings_thresholds_hint": "Valori predefiniti per le nuove Configurazioni suggerite (percentuale residua) e per le batterie della flotta senza soglia Battery Notes propria. Le attività esistenti mantengono il loro valore.", + "bn_summary": "{name}: {pct} % per {n} batterie", + "bn_floor_decides": "La tua soglia del {floor} % decide per tutte; per ogni batteria conta la più alta delle due soglie.", + "bn_above_floor": "È sopra la tua soglia del {floor} %, quindi {name} decide per tutte le batterie annotate.", + "bn_overrides": "{n} dispositivi con soglia propria:", + "bn_more": "+ altri {n}", + "settings_row_actions": "Azioni nelle righe delle attività", + "row_actions_buttons_compact": "Pulsanti (solo icone sul telefono)", + "row_actions_buttons": "Pulsanti con testo", + "row_actions_icons": "Icone (classico)", + "row_actions_follow": "Segui l'impostazione della casa", "settings_panel_enabled": "Pannello laterale", "settings_panel_title": "Titolo pannello laterale", "settings_notifications": "Notifiche", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.", "update_banner": "Sul server è presente una versione più recente di Maintenance Supporter — ricarica per aggiornare il pannello.", "update_reload": "Ricarica", + "row_actions_banner": "Completa e Salta nelle righe delle attività ora sono pulsanti. Preferisci le vecchie icone?", + "row_actions_keep": "Mantieni i pulsanti", + "row_actions_back": "Torna alle icone", "battery_fleet_forecast_overdue": "Data prevista superata — la batteria risulta ancora in buono stato. Se l'hai sostituita, registra la sostituzione; altrimenti la previsione era errata.", "cost_from_parts": "Usa ≈ {amount} dai ricambi", "dismiss": "Ignora", diff --git a/custom_components/maintenance_supporter/frontend/locales/ja.json b/custom_components/maintenance_supporter/frontend/locales/ja.json index 80926b49..8c1d3130 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ja.json +++ b/custom_components/maintenance_supporter/frontend/locales/ja.json @@ -376,6 +376,7 @@ "card_title": "タイトル", "card_show_header": "統計付きヘッダーを表示", "card_show_actions": "操作ボタンを表示", + "card_action_style": "完了ボタンのスタイル", "card_compact": "コンパクトモード", "card_max_items": "最大表示件数(0=すべて)", "card_filter_status": "状態で絞り込み", @@ -450,6 +451,19 @@ "feat_checklists_desc": "タスク完了のための複数手順の手続き", "settings_general": "一般", "settings_default_warning": "既定の警告日数", + "settings_consumable_threshold": "消耗品「残量少」しきい値(%)", + "settings_battery_low_percent": "電池「残量少」しきい値(%)", + "settings_thresholds_hint": "新しい「推奨セットアップ」(残量パーセント)と、独自の Battery Notes しきい値を持たないバッテリー一覧の電池に適用される既定値です。既存のタスクは値を保持します。", + "bn_summary": "{name}:{n} 個の電池に {pct} %", + "bn_floor_decides": "あなたの {floor} % のしきい値がすべてに適用されます。電池ごとに高い方のしきい値が有効です。", + "bn_above_floor": "これはあなたの {floor} % のしきい値を上回るため、記録済みのすべての電池は {name} が決定します。", + "bn_overrides": "独自のしきい値を持つデバイス {n} 台:", + "bn_more": "+ 他 {n} 件", + "settings_row_actions": "タスク行の操作", + "row_actions_buttons_compact": "ボタン(スマートフォンではアイコンのみ)", + "row_actions_buttons": "テキスト付きボタン", + "row_actions_icons": "アイコン(従来)", + "row_actions_follow": "全体設定に従う", "settings_panel_enabled": "サイドバーパネル", "settings_panel_title": "サイドバーパネルのタイトル", "settings_notifications": "通知", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。", "update_banner": "サーバーに新しいバージョンの Maintenance Supporter があります — 再読み込みしてパネルを更新してください。", "update_reload": "再読み込み", + "row_actions_banner": "タスク行の「完了」と「スキップ」がボタンになりました。以前のアイコンに戻しますか?", + "row_actions_keep": "ボタンのままにする", + "row_actions_back": "アイコンに戻す", "battery_fleet_forecast_overdue": "予測日を過ぎましたが、電池はまだ正常と報告しています。交換済みなら交換を記録してください。そうでなければ予測が外れています。", "cost_from_parts": "部品から ≈ {amount} を適用", "dismiss": "閉じる", diff --git a/custom_components/maintenance_supporter/frontend/locales/ko.json b/custom_components/maintenance_supporter/frontend/locales/ko.json index 7a65c8a6..2048aac5 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ko.json +++ b/custom_components/maintenance_supporter/frontend/locales/ko.json @@ -376,6 +376,7 @@ "card_title": "제목", "card_show_header": "통계 헤더 표시", "card_show_actions": "동작 버튼 표시", + "card_action_style": "완료 버튼 스타일", "card_compact": "컴팩트 모드", "card_max_items": "최대 항목 수 (0 = 전체)", "card_filter_status": "상태로 필터", @@ -450,6 +451,19 @@ "feat_checklists_desc": "작업 완료를 위한 다단계 절차", "settings_general": "일반", "settings_default_warning": "기본 경고 일수", + "settings_consumable_threshold": "소모품 „부족“ 임계값 (%)", + "settings_battery_low_percent": "배터리 „부족“ 임계값 (%)", + "settings_thresholds_hint": "새 추천 설정(남은 비율)과 자체 Battery Notes 임계값이 없는 배터리 목록 배터리의 기본값입니다. 기존 작업은 값을 유지합니다.", + "bn_summary": "{name}: 배터리 {n}개에 {pct} %", + "bn_floor_decides": "당신의 {floor} % 임계값이 모두에 적용됩니다. 배터리마다 두 임계값 중 높은 쪽이 적용됩니다.", + "bn_above_floor": "이는 당신의 {floor} % 임계값보다 높으므로, 기록된 모든 배터리는 {name}이(가) 결정합니다.", + "bn_overrides": "자체 임계값이 있는 기기 {n}개:", + "bn_more": "+ {n}개 더", + "settings_row_actions": "작업 행 동작", + "row_actions_buttons_compact": "버튼 (휴대폰에서는 아이콘만)", + "row_actions_buttons": "텍스트가 있는 버튼", + "row_actions_icons": "아이콘 (기본)", + "row_actions_follow": "가정 설정을 따름", "settings_panel_enabled": "사이드바 패널", "settings_panel_title": "사이드바 패널 제목", "settings_notifications": "알림", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.", "update_banner": "서버에 Maintenance Supporter의 새 버전이 있습니다 — 패널을 업데이트하려면 새로 고침하세요.", "update_reload": "새로 고침", + "row_actions_banner": "작업 행의 완료와 건너뛰기가 이제 버튼으로 표시됩니다. 이전 아이콘이 더 좋으신가요?", + "row_actions_keep": "버튼 유지", + "row_actions_back": "아이콘으로 되돌리기", "battery_fleet_forecast_overdue": "예측 날짜가 지났지만 배터리는 여전히 정상으로 보고됩니다. 교체했다면 교체를 기록하세요. 아니라면 예측이 빗나간 것입니다.", "cost_from_parts": "부품에서 ≈ {amount} 적용", "dismiss": "닫기", diff --git a/custom_components/maintenance_supporter/frontend/locales/nb.json b/custom_components/maintenance_supporter/frontend/locales/nb.json index 9bfda8bf..794ea7b4 100644 --- a/custom_components/maintenance_supporter/frontend/locales/nb.json +++ b/custom_components/maintenance_supporter/frontend/locales/nb.json @@ -376,6 +376,7 @@ "card_title": "Tittel", "card_show_header": "Vis topptekst med statistikk", "card_show_actions": "Vis handlingsknapper", + "card_action_style": "Stil på Fullfør-knappen", "card_compact": "Kompakt modus", "card_max_items": "Maks elementer (0 = alle)", "card_filter_status": "Filtrer etter status", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Flertrinnsprosedyrer for fullføring av oppgaver", "settings_general": "Generelt", "settings_default_warning": "Standard varslingsdager", + "settings_consumable_threshold": "Terskel „forbruksvare lav“ (%)", + "settings_battery_low_percent": "Terskel „batteri lavt“ (%)", + "settings_thresholds_hint": "Standardverdier for nye Foreslåtte oppsett (gjenværende prosent) og for flåtebatterier uten egen Battery Notes-terskel. Eksisterende oppgaver beholder verdien sin.", + "bn_summary": "{name}: {pct} % for {n} batterier", + "bn_floor_decides": "Din terskel på {floor} % avgjør for alle; per batteri gjelder den høyeste av de to tersklene.", + "bn_above_floor": "Det er over din terskel på {floor} %, så {name} avgjør for alle noterte batterier.", + "bn_overrides": "{n} enheter med egen terskel:", + "bn_more": "+ {n} til", + "settings_row_actions": "Handlinger i oppgaverader", + "row_actions_buttons_compact": "Knapper (bare ikoner på mobil)", + "row_actions_buttons": "Knapper med tekst", + "row_actions_icons": "Ikoner (klassisk)", + "row_actions_follow": "Følg husstandens innstilling", "settings_panel_enabled": "Sidepanel", "settings_panel_title": "Tittel på sidepanel", "settings_notifications": "Varsler", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.", "update_banner": "En nyere versjon av Maintenance Supporter er på serveren — last inn på nytt for å oppdatere panelet.", "update_reload": "Last inn på nytt", + "row_actions_banner": "Fullfør og Hopp over er nå knapper i oppgaveradene. Foretrekker du de gamle ikonene?", + "row_actions_keep": "Behold knapper", + "row_actions_back": "Tilbake til ikoner", "battery_fleet_forecast_overdue": "Forutsagt dato er passert — batteriet melder fortsatt god tilstand. Hvis du byttet det, registrer byttet; ellers bommet prognosen.", "cost_from_parts": "Bruk ≈ {amount} fra deler", "dismiss": "Avvis", diff --git a/custom_components/maintenance_supporter/frontend/locales/nl.json b/custom_components/maintenance_supporter/frontend/locales/nl.json index 99676394..5f578e71 100644 --- a/custom_components/maintenance_supporter/frontend/locales/nl.json +++ b/custom_components/maintenance_supporter/frontend/locales/nl.json @@ -374,6 +374,7 @@ "card_title": "Titel", "card_show_header": "Koptekst met statistieken tonen", "card_show_actions": "Actieknoppen tonen", + "card_action_style": "Stijl van de Voltooien-knop", "card_compact": "Compacte modus", "card_max_items": "Max items (0 = alle)", "card_filter_status": "Filteren op status", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Meerstaps procedures voor taakvoltooiing", "settings_general": "Algemeen", "settings_default_warning": "Standaard waarschuwingsdagen", + "settings_consumable_threshold": "Drempel „verbruiksartikel laag“ (%)", + "settings_battery_low_percent": "Drempel „batterij laag“ (%)", + "settings_thresholds_hint": "Standaardwaarden voor nieuwe Voorgestelde installaties (resterend percentage) en voor vlootbatterijen zonder eigen Battery Notes-drempel. Bestaande taken behouden hun waarde.", + "bn_summary": "{name}: {pct} % voor {n} batterijen", + "bn_floor_decides": "Jouw drempel van {floor} % beslist voor allemaal; per batterij telt de hoogste van de twee drempels.", + "bn_above_floor": "Dat ligt boven jouw drempel van {floor} %, dus {name} beslist voor alle genoteerde batterijen.", + "bn_overrides": "{n} apparaten met een eigen drempel:", + "bn_more": "+ {n} meer", + "settings_row_actions": "Acties in taakrijen", + "row_actions_buttons_compact": "Knoppen (op telefoons alleen pictogrammen)", + "row_actions_buttons": "Knoppen met tekst", + "row_actions_icons": "Pictogrammen (klassiek)", + "row_actions_follow": "Volg de instelling van het huishouden", "settings_panel_enabled": "Zijbalkpaneel", "settings_panel_title": "Titel zijbalkpaneel", "settings_notifications": "Meldingen", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.", "update_banner": "Er staat een nieuwere versie van Maintenance Supporter op de server — herlaad om het paneel bij te werken.", "update_reload": "Herladen", + "row_actions_banner": "Voltooien en Overslaan zijn in taakrijen nu knoppen. Liever de oude pictogrammen?", + "row_actions_keep": "Knoppen houden", + "row_actions_back": "Terug naar pictogrammen", "battery_fleet_forecast_overdue": "Voorspelde datum verstreken — de batterij meldt zich nog steeds gezond. Heb je hem vervangen, registreer dan de vervanging; anders zat de voorspelling ernaast.", "cost_from_parts": "≈ {amount} uit onderdelen overnemen", "dismiss": "Verbergen", diff --git a/custom_components/maintenance_supporter/frontend/locales/pl.json b/custom_components/maintenance_supporter/frontend/locales/pl.json index 5cb7fc14..f995872f 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pl.json +++ b/custom_components/maintenance_supporter/frontend/locales/pl.json @@ -374,6 +374,7 @@ "card_title": "Tytuł", "card_show_header": "Pokaż nagłówek ze statystykami", "card_show_actions": "Pokaż przyciski akcji", + "card_action_style": "Styl przycisku Ukończ", "card_compact": "Tryb kompaktowy", "card_max_items": "Maks. elementów (0 = wszystkie)", "card_filter_status": "Filtruj wg statusu", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Wieloetapowe procedury wykonania zadania", "settings_general": "Ogólne", "settings_default_warning": "Domyślne dni ostrzeżenia", + "settings_consumable_threshold": "Próg „materiał eksploatacyjny niski” (%)", + "settings_battery_low_percent": "Próg „bateria słaba” (%)", + "settings_thresholds_hint": "Wartości domyślne dla nowych Sugerowanych konfiguracji (procent pozostały) i dla baterii floty bez własnego progu Battery Notes. Istniejące zadania zachowują swoją wartość.", + "bn_summary": "{name}: {pct} % dla {n} baterii", + "bn_floor_decides": "Twój próg {floor} % decyduje dla wszystkich; dla każdej baterii liczy się wyższy z dwóch progów.", + "bn_above_floor": "To powyżej twojego progu {floor} %, więc o wszystkich odnotowanych bateriach decyduje {name}.", + "bn_overrides": "{n} urządzeń z własnym progiem:", + "bn_more": "+ {n} więcej", + "settings_row_actions": "Akcje w wierszach zadań", + "row_actions_buttons_compact": "Przyciski (na telefonie tylko ikony)", + "row_actions_buttons": "Przyciski z tekstem", + "row_actions_icons": "Ikony (klasyczne)", + "row_actions_follow": "Zgodnie z ustawieniem domu", "settings_panel_enabled": "Panel boczny", "settings_panel_title": "Tytuł panelu bocznego", "settings_notifications": "Powiadomienia", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.", "update_banner": "Na serwerze jest nowsza wersja Maintenance Supporter — załaduj ponownie, aby zaktualizować panel.", "update_reload": "Załaduj ponownie", + "row_actions_banner": "Ukończ i Pomiń w wierszach zadań są teraz przyciskami. Wolisz stare ikony?", + "row_actions_keep": "Zachowaj przyciski", + "row_actions_back": "Wróć do ikon", "battery_fleet_forecast_overdue": "Przewidywana data minęła — bateria nadal zgłasza dobry stan. Jeśli ją wymieniono, zapisz wymianę; w przeciwnym razie prognoza była błędna.", "cost_from_parts": "Użyj ≈ {amount} z części", "dismiss": "Odrzuć", diff --git a/custom_components/maintenance_supporter/frontend/locales/pt-br.json b/custom_components/maintenance_supporter/frontend/locales/pt-br.json index 22832fc0..aa9668a0 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pt-br.json +++ b/custom_components/maintenance_supporter/frontend/locales/pt-br.json @@ -376,6 +376,7 @@ "card_title": "Título", "card_show_header": "Mostrar cabeçalho com estatísticas", "card_show_actions": "Mostrar botões de ação", + "card_action_style": "Estilo do botão Concluir", "card_compact": "Modo compacto", "card_max_items": "Máx. de itens (0 = todos)", "card_filter_status": "Filtrar por status", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Procedimentos em várias etapas para concluir tarefas", "settings_general": "Geral", "settings_default_warning": "Dias de aviso padrão", + "settings_consumable_threshold": "Limite „consumível baixo“ (%)", + "settings_battery_low_percent": "Limite „pilha fraca“ (%)", + "settings_thresholds_hint": "Padrões para novas Configurações sugeridas (porcentagem restante) e para pilhas da frota sem limite próprio do Battery Notes. As tarefas existentes mantêm seu valor.", + "bn_summary": "{name}: {pct} % para {n} pilhas", + "bn_floor_decides": "Seu limite de {floor} % decide para todas; por pilha vale o maior dos dois limites.", + "bn_above_floor": "Está acima do seu limite de {floor} %, então o {name} decide para todas as pilhas anotadas.", + "bn_overrides": "{n} dispositivos com limite próprio:", + "bn_more": "+ {n} mais", + "settings_row_actions": "Ações nas linhas de tarefas", + "row_actions_buttons_compact": "Botões (só ícones no celular)", + "row_actions_buttons": "Botões com texto", + "row_actions_icons": "Ícones (clássico)", + "row_actions_follow": "Seguir a configuração da casa", "settings_panel_enabled": "Painel na barra lateral", "settings_panel_title": "Título do painel na barra lateral", "settings_notifications": "Notificações", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.", "update_banner": "Há uma versão mais nova do Maintenance Supporter no servidor — recarregue para atualizar o painel.", "update_reload": "Recarregar", + "row_actions_banner": "Concluir e Pular agora são botões nas linhas de tarefas. Prefere os ícones antigos?", + "row_actions_keep": "Manter botões", + "row_actions_back": "Voltar aos ícones", "battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria ainda reporta bom estado. Se você a trocou, registre a troca; caso contrário, a previsão errou.", "cost_from_parts": "Usar ≈ {amount} das peças", "dismiss": "Dispensar", diff --git a/custom_components/maintenance_supporter/frontend/locales/pt.json b/custom_components/maintenance_supporter/frontend/locales/pt.json index 6092d6f1..19f178a3 100644 --- a/custom_components/maintenance_supporter/frontend/locales/pt.json +++ b/custom_components/maintenance_supporter/frontend/locales/pt.json @@ -374,6 +374,7 @@ "card_title": "Título", "card_show_header": "Mostrar cabeçalho com estatísticas", "card_show_actions": "Mostrar botões de ação", + "card_action_style": "Estilo do botão Concluir", "card_compact": "Modo compacto", "card_max_items": "Máx. itens (0 = todos)", "card_filter_status": "Filtrar por estado", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Procedimentos com vários passos para conclusão de tarefas", "settings_general": "Geral", "settings_default_warning": "Dias de aviso predefinidos", + "settings_consumable_threshold": "Limiar „consumível baixo“ (%)", + "settings_battery_low_percent": "Limiar „pilha fraca“ (%)", + "settings_thresholds_hint": "Predefinições para novas Configurações sugeridas (percentagem restante) e para pilhas da frota sem limiar Battery Notes próprio. As tarefas existentes mantêm o seu valor.", + "bn_summary": "{name}: {pct} % para {n} pilhas", + "bn_floor_decides": "O teu limiar de {floor} % decide para todas; por pilha conta o mais alto dos dois limiares.", + "bn_above_floor": "Está acima do teu limiar de {floor} %, por isso o {name} decide para todas as pilhas anotadas.", + "bn_overrides": "{n} dispositivos com limiar próprio:", + "bn_more": "+ {n} mais", + "settings_row_actions": "Ações nas linhas de tarefas", + "row_actions_buttons_compact": "Botões (só ícones no telemóvel)", + "row_actions_buttons": "Botões com texto", + "row_actions_icons": "Ícones (clássico)", + "row_actions_follow": "Seguir a definição da casa", "settings_panel_enabled": "Painel lateral", "settings_panel_title": "Título do painel lateral", "settings_notifications": "Notificações", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.", "update_banner": "Há uma versão mais recente do Maintenance Supporter no servidor — recarregue para atualizar o painel.", "update_reload": "Recarregar", + "row_actions_banner": "Concluir e Ignorar são agora botões nas linhas de tarefas. Prefere os ícones antigos?", + "row_actions_keep": "Manter botões", + "row_actions_back": "Voltar aos ícones", "battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria continua a reportar bom estado. Se a substituiu, registe a substituição; caso contrário, a previsão falhou.", "cost_from_parts": "Usar ≈ {amount} das peças", "dismiss": "Dispensar", diff --git a/custom_components/maintenance_supporter/frontend/locales/ru.json b/custom_components/maintenance_supporter/frontend/locales/ru.json index f2984d0c..86314351 100644 --- a/custom_components/maintenance_supporter/frontend/locales/ru.json +++ b/custom_components/maintenance_supporter/frontend/locales/ru.json @@ -374,6 +374,7 @@ "card_title": "Заголовок", "card_show_header": "Показывать заголовок со статистикой", "card_show_actions": "Показывать кнопки действий", + "card_action_style": "Стиль кнопки «Выполнено»", "card_compact": "Компактный режим", "card_max_items": "Макс. элементов (0 = все)", "card_filter_status": "Фильтровать по статусу", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Многошаговые процедуры для выполнения задачи", "settings_general": "Основные", "settings_default_warning": "Дни предупреждения по умолчанию", + "settings_consumable_threshold": "Порог «расходник заканчивается» (%)", + "settings_battery_low_percent": "Порог «батарея разряжена» (%)", + "settings_thresholds_hint": "Значения по умолчанию для новых «Предлагаемых настроек» (остаток в процентах) и для батарей парка без собственного порога Battery Notes. Существующие задачи сохраняют своё значение.", + "bn_summary": "{name}: {pct} % для {n} батарей", + "bn_floor_decides": "Ваш порог {floor} % действует для всех; для каждой батареи учитывается больший из двух порогов.", + "bn_above_floor": "Это выше вашего порога {floor} %, поэтому для всех отмеченных батарей решает {name}.", + "bn_overrides": "{n} устройств с собственным порогом:", + "bn_more": "+ ещё {n}", + "settings_row_actions": "Действия в строках задач", + "row_actions_buttons_compact": "Кнопки (на телефоне только значки)", + "row_actions_buttons": "Кнопки с текстом", + "row_actions_icons": "Значки (классический вид)", + "row_actions_follow": "Как в настройках дома", "settings_panel_enabled": "Боковая панель", "settings_panel_title": "Заголовок панели", "settings_notifications": "Уведомления", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.", "update_banner": "На сервере более новая версия Maintenance Supporter — перезагрузите, чтобы обновить панель.", "update_reload": "Перезагрузить", + "row_actions_banner": "«Выполнено» и «Пропустить» в строках задач теперь кнопки. Предпочитаете старые значки?", + "row_actions_keep": "Оставить кнопки", + "row_actions_back": "Вернуть значки", "battery_fleet_forecast_overdue": "Прогнозируемая дата прошла — батарея по-прежнему сообщает о хорошем состоянии. Если вы её заменили, зафиксируйте замену; иначе прогноз оказался неверным.", "cost_from_parts": "Взять ≈ {amount} из запчастей", "dismiss": "Скрыть", diff --git a/custom_components/maintenance_supporter/frontend/locales/sv.json b/custom_components/maintenance_supporter/frontend/locales/sv.json index 00016788..62a08fc5 100644 --- a/custom_components/maintenance_supporter/frontend/locales/sv.json +++ b/custom_components/maintenance_supporter/frontend/locales/sv.json @@ -374,6 +374,7 @@ "card_title": "Titel", "card_show_header": "Visa rubrik med statistik", "card_show_actions": "Visa åtgärdsknappar", + "card_action_style": "Stil på Slutför-knappen", "card_compact": "Kompakt läge", "card_max_items": "Max objekt (0 = alla)", "card_filter_status": "Filtrera efter status", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Flerstegs procedurer för uppgiftens slutförande", "settings_general": "Allmänt", "settings_default_warning": "Standard varningsdagar", + "settings_consumable_threshold": "Tröskel „förbrukningsartikel låg“ (%)", + "settings_battery_low_percent": "Tröskel „batteri lågt“ (%)", + "settings_thresholds_hint": "Standardvärden för nya Föreslagna inställningar (återstående procent) och för flottans batterier utan egen Battery Notes-tröskel. Befintliga uppgifter behåller sitt värde.", + "bn_summary": "{name}: {pct} % för {n} batterier", + "bn_floor_decides": "Din tröskel på {floor} % avgör för alla; per batteri gäller den högre av de två trösklarna.", + "bn_above_floor": "Det är över din tröskel på {floor} %, så {name} avgör för alla noterade batterier.", + "bn_overrides": "{n} enheter med egen tröskel:", + "bn_more": "+ {n} till", + "settings_row_actions": "Åtgärder i uppgiftsrader", + "row_actions_buttons_compact": "Knappar (endast ikoner på mobilen)", + "row_actions_buttons": "Knappar med text", + "row_actions_icons": "Ikoner (klassiskt)", + "row_actions_follow": "Följ hushållets inställning", "settings_panel_enabled": "Sidopanel", "settings_panel_title": "Sidopanelens titel", "settings_notifications": "Notifikationer", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.", "update_banner": "En nyare version av Maintenance Supporter finns på servern — ladda om för att uppdatera panelen.", "update_reload": "Ladda om", + "row_actions_banner": "Slutför och Hoppa över är nu knappar i uppgiftsraderna. Föredrar du de gamla ikonerna?", + "row_actions_keep": "Behåll knappar", + "row_actions_back": "Tillbaka till ikoner", "battery_fleet_forecast_overdue": "Förutsagt datum har passerat — batteriet rapporterar fortfarande god status. Om du bytte det, registrera bytet; annars slog prognosen fel.", "cost_from_parts": "Använd ≈ {amount} från delar", "dismiss": "Avfärda", diff --git a/custom_components/maintenance_supporter/frontend/locales/tr.json b/custom_components/maintenance_supporter/frontend/locales/tr.json index e650b5f7..08502906 100644 --- a/custom_components/maintenance_supporter/frontend/locales/tr.json +++ b/custom_components/maintenance_supporter/frontend/locales/tr.json @@ -376,6 +376,7 @@ "card_title": "Başlık", "card_show_header": "İstatistikli başlığı göster", "card_show_actions": "Eylem düğmelerini göster", + "card_action_style": "Tamamla düğmesi stili", "card_compact": "Kompakt mod", "card_max_items": "En fazla öğe (0 = tümü)", "card_filter_status": "Duruma göre filtrele", @@ -450,6 +451,19 @@ "feat_checklists_desc": "Görev tamamlama için çok adımlı prosedürler", "settings_general": "Genel", "settings_default_warning": "Varsayılan uyarı günleri", + "settings_consumable_threshold": "„Sarf malzemesi düşük“ eşiği (%)", + "settings_battery_low_percent": "„Pil zayıf“ eşiği (%)", + "settings_thresholds_hint": "Yeni Önerilen kurulumlar (kalan yüzde) ve kendi Battery Notes eşiği olmayan filo pilleri için varsayılanlar. Mevcut görevler değerlerini korur.", + "bn_summary": "{name}: {n} pil için % {pct}", + "bn_floor_decides": "Senin % {floor} eşiğin hepsi için belirleyici; her pil için iki eşikten yüksek olanı geçerli.", + "bn_above_floor": "Bu, senin % {floor} eşiğinin üzerinde; bu yüzden kayıtlı tüm piller için {name} belirleyici.", + "bn_overrides": "Kendi eşiği olan {n} cihaz:", + "bn_more": "+ {n} tane daha", + "settings_row_actions": "Görev satırı eylemleri", + "row_actions_buttons_compact": "Düğmeler (telefonda yalnızca simgeler)", + "row_actions_buttons": "Metinli düğmeler", + "row_actions_icons": "Simgeler (klasik)", + "row_actions_follow": "Ev ayarını izle", "settings_panel_enabled": "Kenar çubuğu paneli", "settings_panel_title": "Kenar çubuğu paneli başlığı", "settings_notifications": "Bildirimler", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.", "update_banner": "Sunucuda Maintenance Supporter'ın daha yeni bir sürümü var — paneli güncellemek için sayfayı yeniden yükleyin.", "update_reload": "Yeniden yükle", + "row_actions_banner": "Görev satırlarındaki Tamamla ve Atla artık düğme. Eski simgeleri mi tercih edersiniz?", + "row_actions_keep": "Düğmeleri koru", + "row_actions_back": "Simgelere geri dön", "battery_fleet_forecast_overdue": "Öngörülen tarih geçti — pil hâlâ sağlıklı görünüyor. Değiştirdiyseniz değişimi kaydedin; aksi halde tahmin yanılmış demektir.", "cost_from_parts": "Parçalardan ≈ {amount} kullan", "dismiss": "Kapat", diff --git a/custom_components/maintenance_supporter/frontend/locales/uk.json b/custom_components/maintenance_supporter/frontend/locales/uk.json index 52fe1d8d..9789afa2 100644 --- a/custom_components/maintenance_supporter/frontend/locales/uk.json +++ b/custom_components/maintenance_supporter/frontend/locales/uk.json @@ -374,6 +374,7 @@ "card_title": "Заголовок", "card_show_header": "Показувати заголовок зі статистикою", "card_show_actions": "Показувати кнопки дій", + "card_action_style": "Стиль кнопки «Виконано»", "card_compact": "Компактний режим", "card_max_items": "Макс. елементів (0 = всі)", "card_filter_status": "Фільтрувати за статусом", @@ -448,6 +449,19 @@ "feat_checklists_desc": "Багатокрокові процедури для виконання завдань", "settings_general": "Загальне", "settings_default_warning": "Днів попередження за замовчуванням", + "settings_consumable_threshold": "Поріг «витратний матеріал закінчується» (%)", + "settings_battery_low_percent": "Поріг «батарея розряджена» (%)", + "settings_thresholds_hint": "Значення за замовчуванням для нових «Запропонованих налаштувань» (залишок у відсотках) і для батарей парку без власного порогу Battery Notes. Наявні завдання зберігають своє значення.", + "bn_summary": "{name}: {pct} % для {n} батарей", + "bn_floor_decides": "Ваш поріг {floor} % діє для всіх; для кожної батареї враховується більший із двох порогів.", + "bn_above_floor": "Це вище за ваш поріг {floor} %, тому для всіх занотованих батарей вирішує {name}.", + "bn_overrides": "{n} пристроїв із власним порогом:", + "bn_more": "+ ще {n}", + "settings_row_actions": "Дії в рядках завдань", + "row_actions_buttons_compact": "Кнопки (на телефоні лише значки)", + "row_actions_buttons": "Кнопки з текстом", + "row_actions_icons": "Значки (класичний вигляд)", + "row_actions_follow": "Як у налаштуваннях дому", "settings_panel_enabled": "Панель у бічному меню", "settings_panel_title": "Заголовок панелі", "settings_notifications": "Сповіщення", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.", "update_banner": "На сервері новіша версія Maintenance Supporter — перезавантажте, щоб оновити панель.", "update_reload": "Перезавантажити", + "row_actions_banner": "«Виконано» та «Пропустити» в рядках завдань тепер кнопки. Бажаєте старі значки?", + "row_actions_keep": "Залишити кнопки", + "row_actions_back": "Повернути значки", "battery_fleet_forecast_overdue": "Прогнозована дата минула — батарея й далі повідомляє про добрий стан. Якщо ви її замінили, зафіксуйте заміну; інакше прогноз не справдився.", "cost_from_parts": "Узяти ≈ {amount} із запчастин", "dismiss": "Приховати", diff --git a/custom_components/maintenance_supporter/frontend/locales/zh.json b/custom_components/maintenance_supporter/frontend/locales/zh.json index 48d8baa0..38de2aa1 100644 --- a/custom_components/maintenance_supporter/frontend/locales/zh.json +++ b/custom_components/maintenance_supporter/frontend/locales/zh.json @@ -376,6 +376,7 @@ "card_title": "标题", "card_show_header": "显示统计信息页眉", "card_show_actions": "显示操作按钮", + "card_action_style": "“完成”按钮样式", "card_compact": "紧凑模式", "card_max_items": "最大显示项 (0 = 全部)", "card_filter_status": "按状态过滤", @@ -450,6 +451,19 @@ "feat_checklists_desc": "为任务完成提供多步骤操作流程", "settings_general": "常规", "settings_default_warning": "默认预警天数", + "settings_consumable_threshold": "耗材“不足”阈值(%)", + "settings_battery_low_percent": "电池“电量低”阈值(%)", + "settings_thresholds_hint": "用于新的“建议设置”(剩余百分比)以及没有自身 Battery Notes 阈值的电池组电池。现有任务保留其值。", + "bn_summary": "{name}:{n} 块电池为 {pct}%", + "bn_floor_decides": "你的 {floor}% 阈值对所有电池生效;每块电池取两个阈值中较高者。", + "bn_above_floor": "这高于你的 {floor}% 阈值,因此所有已记录的电池由 {name} 决定。", + "bn_overrides": "{n} 台设备有自己的阈值:", + "bn_more": "+ 另外 {n} 个", + "settings_row_actions": "任务行操作", + "row_actions_buttons_compact": "按钮(手机上仅显示图标)", + "row_actions_buttons": "带文字的按钮", + "row_actions_icons": "图标(经典)", + "row_actions_follow": "跟随全局设置", "settings_panel_enabled": "侧边栏面板", "settings_panel_title": "侧边栏面板标题", "settings_notifications": "通知", @@ -864,6 +878,9 @@ "battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。", "update_banner": "服务器上有更新版本的 Maintenance Supporter——请重新加载以更新面板。", "update_reload": "重新加载", + "row_actions_banner": "任务行中的“完成”和“跳过”现在是按钮。更喜欢旧的图标?", + "row_actions_keep": "保留按钮", + "row_actions_back": "改回图标", "battery_fleet_forecast_overdue": "预测日期已过——电池仍报告状态良好。如果您已更换电池,请记录更换;否则说明预测有误。", "cost_from_parts": "采用配件合计 ≈ {amount}", "dismiss": "忽略", diff --git a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js index 0e78aa90..fb573f92 100644 --- a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js +++ b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js @@ -1,9 +1,9 @@ -/*! maintenance_supporter frontend 2.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??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(s=>s.hostConnected?.())}enableUpdating(s){}disconnectedCallback(){this._$EO?.forEach(s=>s.hostDisconnected?.())}attributeChangedCallback(s,e,t){this._$AK(s,t)}_$ET(s,e){let t=this.constructor.elementProperties.get(s),i=this.constructor._$Eu(s,t);if(i!==void 0&&t.reflect===!0){let n=(t.converter?.toAttribute!==void 0?t.converter:$e).toAttribute(e,t.type);this._$Em=s,n==null?this.removeAttribute(i):this.setAttribute(i,n),this._$Em=null}}_$AK(s,e){let t=this.constructor,i=t._$Eh.get(s);if(i!==void 0&&this._$Em!==i){let n=t.getPropertyOptions(i),o=typeof n.converter=="function"?{fromAttribute:n.converter}:n.converter?.fromAttribute!==void 0?n.converter:$e;this._$Em=i;let p=o.fromAttribute(e,n.type);this[i]=p??this._$Ej?.get(i)??p,this._$Em=null}}requestUpdate(s,e,t,i=!1,n){if(s!==void 0){let o=this.constructor;if(i===!1&&(n=this[s]),t??=o.getPropertyOptions(s),!((t.hasChanged??Fe)(n,e)||t.useDefault&&t.reflect&&n===this._$Ej?.get(s)&&!this.hasAttribute(o._$Eu(s,t))))return;this.C(s,e,t)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(s,e,{useDefault:t,reflect:i,wrapped:n},o){t&&!(this._$Ej??=new Map).has(s)&&(this._$Ej.set(s,o??e??this[s]),n!==!0||o!==void 0)||(this._$AL.has(s)||(this.hasUpdated||t||(e=void 0),this._$AL.set(s,e)),i===!0&&this._$Em!==s&&(this._$Eq??=new Set).add(s))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let s=this.scheduleUpdate();return s!=null&&await s,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[i,n]of this._$Ep)this[i]=n;this._$Ep=void 0}let t=this.constructor.elementProperties;if(t.size>0)for(let[i,n]of t){let{wrapped:o}=n,p=this[i];o!==!0||this._$AL.has(i)||p===void 0||this.C(i,void 0,n,p)}}let s=!1,e=this._$AL;try{s=this.shouldUpdate(e),s?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(t){throw s=!1,this._$EM(),t}s&&this._$AE(e)}willUpdate(s){}_$AE(s){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(s)),this.updated(s)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(s){return!0}update(s){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(s){}firstUpdated(s){}};Y.elementStyles=[],Y.shadowRootOptions={mode:"open"},Y[we("elementProperties")]=new Map,Y[we("finalized")]=new Map,vs?.({ReactiveElement:Y}),(Oe.reactiveElementVersions??=[]).push("2.1.2")});function zt(r,s){if(!ct(r)||!r.hasOwnProperty("raw"))throw Error("invalid template strings array");return qt!==void 0?qt.createHTML(s):s}function ue(r,s,e=r,t){if(s===oe)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,n=Ae(s)?void 0:s._$litDirective$;return i?.constructor!==n&&(i?._$AO?.(!1),n===void 0?i=void 0:(i=new n(r),i._$AT(r,e,t)),t!==void 0?(e._$Co??=[])[t]=i:e._$Cl=i),i!==void 0&&(s=ue(r,i._$AS(r,s.values),i,t)),s}var lt,Lt,ze,qt,Mt,ie,Ot,ys,ne,Se,Ae,ct,bs,it,Ee,Rt,jt,re,Nt,Ht,Ft,dt,l,_e,Er,oe,_,Dt,ae,xs,Te,st,Ie,he,rt,at,nt,ot,ws,Ut,Ue=w(()=>{lt=globalThis,Lt=r=>r,ze=lt.trustedTypes,qt=ze?ze.createPolicy("lit-html",{createHTML:r=>r}):void 0,Mt="$lit$",ie=`lit$${Math.random().toFixed(9).slice(2)}$`,Ot="?"+ie,ys=`<${Ot}>`,ne=document,Se=()=>ne.createComment(""),Ae=r=>r===null||typeof r!="object"&&typeof r!="function",ct=Array.isArray,bs=r=>ct(r)||typeof r?.[Symbol.iterator]=="function",it=`[ -\f\r]`,Ee=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,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?"":s===3?"":"",o=Ee;for(let p=0;p"?(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?"":s===3?"":"")),t]},Te=class r{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let n=0,o=0,p=s.length-1,d=this.parts,[u,m]=xs(s,e);if(this.el=r.createElement(u,t),ae.currentNode=this.el.content,e===2||e===3){let f=this.el.content.firstChild;f.replaceWith(...f.childNodes)}for(;(i=ae.nextNode())!==null&&d.length0){i.textContent=ze?ze.emptyScript:"";for(let y=0;y2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=_}_$AI(s,e=this,t,i){let n=this.strings,o=!1;if(n===void 0)s=ue(this,s,e,0),o=!Ae(s)||s!==this._$AH&&s!==oe,o&&(this._$AH=s);else{let p=s,d,u;for(s=n[0],d=0;d{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let n=e?.renderBefore??null;t._$litPart$=i=new Ie(s.insertBefore(Se(),n),n,void 0,e??{})}return i._$AI(r),i}});var pt,A,$s,Bt=w(()=>{ke();ke();Ue();Ue();pt=globalThis,A=class extends Y{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let s=super.createRenderRoot();return this.renderOptions.renderBefore??=s.firstChild,s}update(s){let e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(s),this._$Do=Ut(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return oe}};A._$litElement$=!0,A.finalized=!0,pt.litElementHydrateSupport?.({LitElement:A});$s=pt.litElementPolyfillSupport;$s?.({LitElement:A});(pt.litElementVersions??=[]).push("4.2.2")});var Wt=w(()=>{});var P=w(()=>{ke();Ue();Bt();Wt()});var Vt=w(()=>{});function x(r){return(s,e)=>typeof e=="object"?Es(r,s,e):((t,i,n)=>{let o=i.hasOwnProperty(n);return i.constructor.createProperty(n,t),o?Object.getOwnPropertyDescriptor(i,n):void 0})(r,s,e)}var ks,Es,ut=w(()=>{ke();ks={attribute:!0,type:String,converter:$e,reflect:!1,hasChanged:Fe},Es=(r=ks,s,e)=>{let{kind:t,metadata:i}=e,n=globalThis.litPropertyMetadata.get(i);if(n===void 0&&globalThis.litPropertyMetadata.set(i,n=new Map),t==="setter"&&((r=Object.create(r)).wrapped=!0),n.set(e.name,r),t==="accessor"){let{name:o}=e;return{set(p){let d=s.get.call(this);s.set.call(this,p),this.requestUpdate(o,d,r,!0,p)},init(p){return p!==void 0&&this.C(o,void 0,r,p),p}}}if(t==="setter"){let{name:o}=e;return function(p){let d=this[o];s.call(this,p),this.requestUpdate(o,d,r,!0,p)}}throw Error("Unsupported decorator location: "+t)}});function h(r){return x({...r,state:!0,attribute:!1})}var Kt=w(()=>{ut();});var Gt=w(()=>{});var ge=w(()=>{});var Yt=w(()=>{ge();});var Qt=w(()=>{ge();});var Jt=w(()=>{ge();});var Zt=w(()=>{ge();});var Xt=w(()=>{ge();});var W=w(()=>{Vt();ut();Kt();Gt();Yt();Qt();Jt();Zt();Xt()});function ei(r,s){return!r||r<=0?0:r*(Ss[s||"days"]??1)}var Ss,ti=w(()=>{"use strict";Ss={days:1,weeks:7,months:30.4368,years:365.25}});function le(r){let s=r.getFullYear(),e=String(r.getMonth()+1).padStart(2,"0"),t=String(r.getDate()).padStart(2,"0");return`${s}-${e}-${t}`}function As(r,s){let e=[];for(let t=0;te.cost).filter(e=>typeof e=="number");return s.length===0?null:s.reduce((e,t)=>e+t,0)/s.length}function Is(r){let{windowStart:s,windowEnd:e,task:t,entryId:i,objectName:n}=r,o=[],p=(f,v)=>({date:f,entry_id:i,task_id:t.id,task_name:t.name,object_name:n,status:v&&(t.status==="overdue"||t.status==="triggered")?"ok":t.status,days_until_due:v?null:t.days_until_due??null,projected:v,schedule_type:t.schedule_type,interval_days:t.interval_days??null,interval_unit:t.interval_unit??null,responsible_user_id:t.responsible_user_id??null,avg_cost:Ts(t.history),adaptive_enabled:!!t.adaptive_config?.enabled,prediction_confidence:t.threshold_prediction_confidence??null}),d=Math.max(1,Math.round(ei(t.interval_days,t.interval_unit)));if(t.status==="overdue"||t.status==="triggered"){if(o.push(p(s,!1)),t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=We(s,d),v=1;for(;f<=e&&v=s&&m<=e)o.push(p(m,!1));else if(m>e)return o;if(t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=We(m,d),v=o.length;for(;f<=e&&v=s&&(o.push(p(f,!0)),v++),f=We(f,d)}return o}function ri(r,s,e,t=null){let i=As(s,e),n=i[0],o=i[i.length-1],p=[];for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t||y.enabled===!1)continue;let k=Is({windowStart:n,windowEnd:o,task:y,entryId:f,objectName:m});p.push(...k)}}let d=new Map;for(let u of i)d.set(u,[]);for(let u of p){let m=d.get(u.date);m&&m.push(u)}for(let[,u]of d)u.sort((m,f)=>{let v=si[m.status]??99,y=si[f.status]??99;if(v!==y)return v-y;if(m.projected!==f.projected)return m.projected?1:-1;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:d.get(u)??[]}))}function Ps(r,s){let e=[];for(let t=s-1;t>=0;t--){let i=new Date(r);i.setDate(i.getDate()-t),i.setHours(0,0,0,0),e.push(le(i))}return e}function ai(r,s,e,t=null){let i=Ps(s,e),n=i[0],o=i[i.length-1],p=new Map;for(let u of i)p.set(u,[]);for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t)continue;let k=y.history||[];for(let E of k){if(typeof E?.timestamp!="string")continue;let H=E.timestamp.slice(0,10);if(Ho)continue;let M=p.get(H);if(!M)continue;let q=E.type??"completed";M.push({date:H,entry_id:f,task_id:y.id,task_name:y.name,object_name:m,status:Cs[q]??"ok",days_until_due:null,projected:!1,schedule_type:y.schedule_type,interval_days:y.interval_days??null,responsible_user_id:y.responsible_user_id??null,avg_cost:typeof E.cost=="number"?E.cost:null,adaptive_enabled:!!y.adaptive_config?.enabled,prediction_confidence:null,history_timestamp:E.timestamp,history_type:q,history_cost:typeof E.cost=="number"?E.cost:null,history_notes:typeof E.notes=="string"?E.notes:null,history_duration:typeof E.duration=="number"?E.duration:null})}}}let d={completed:0,reset:1,skipped:2,triggered:3,trigger_replaced:4};for(let[,u]of p)u.sort((m,f)=>{let v=d[m.history_type??""]??99,y=d[f.history_type??""]??99;if(v!==y)return v-y;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:p.get(u)??[]}))}var ii,si,Cs,ht=w(()=>{"use strict";ti();ii=5;si={overdue:0,triggered:1,due_soon:2,ok:3};Cs={completed:"ok",reset:"ok",skipped:"due_soon",missed:"overdue",triggered:"triggered",trigger_replaced:"triggered",trigger_removed:"ok"}});var li,oi=w(()=>{li={maintenance:"Maintenance",objects:"Objects",tasks:"Tasks",overdue:"Overdue",due_soon:"Due Soon",triggered:"Triggered",trigger_replaced:"Trigger replaced",trigger_removed:"Trigger removed",ok:"OK",all:"All",new_object:"+ New Object",templates_from:"From template",templates_title:"Start from a template",templates_task_count:"{n} tasks",template_created:"Created from template",onboard_hint:"Add your first object to start tracking maintenance.",edit:"Edit",duplicate:"Duplicate",task_duplicated:"Task duplicated",object_duplicated:"Object duplicated",delete:"Delete",add_task:"+ Add Task",complete:"Complete",completed:"Completed",skip:"Skip",skipped:"Skipped",missed:"Missed",reset:"Reset",snooze:"Snooze",snoozed:"Snoozed",cancel:"Cancel",bulk_select:"Select",bulk_select_all:"Select all",bulk_n_selected:"{n} selected",bulk_completed:"{n} tasks completed",bulk_archived:"{n} tasks archived",completing:"Completing\u2026",interval:"Interval",warning:"Warning",last_performed:"Last performed",next_due:"Next due",days_until_due:"Days until due",avg_duration:"Avg duration",trigger:"Trigger",trigger_type:"Trigger type",threshold_above:"Upper limit",threshold_below:"Lower limit",threshold:"Threshold",counter:"Counter",state_change:"State change",runtime:"Runtime",runtime_hours:"Target runtime (hours)",target_value:"Target value",baseline:"Baseline",target_changes:"Target changes",for_minutes:"For (minutes)",time_based:"Time-based",sensor_based:"Sensor-based",manual:"Manual",one_time:"One-time",weekdays:"Weekdays",nth_weekday:"Nth weekday of month",day_of_month:"Day of month",recurrence_on_days:"Repeat on",recurrence_occurrence:"Occurrence",recurrence_weekday:"Weekday",recurrence_day:"Day of month (1\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N days, e.g. -2 = two days before.",last_day_month:"Last day of month",last_business_day_month:"Last business day",ord_1:"1st",ord_2:"2nd",ord_3:"3rd",ord_4:"4th",ord_5:"5th",ord_last:"Last",day_word:"Day",interval_value:"Interval",interval_unit:"Unit",unit_days:"Days",unit_weeks:"Weeks",unit_months:"Months",unit_years:"Years",due_date:"Due date",cleaning:"Cleaning",inspection:"Inspection",replacement:"Replacement",calibration:"Calibration",service:"Service",reading:"Reading",custom:"Custom",history:"History",cost:"Cost",report_button:"Report",report_title:"Maintenance report",report_generated:"Generated",report_times_done:"Done",report_total_cost:"Total cost",report_every:"every {n} {unit}",report_notes:"Notes",report_col_type:"Type",report_col_status:"Status",report_col_schedule:"Schedule",duration:"Duration",both:"Both",trigger_val:"Trigger value",complete_title:"Complete: ",checklist:"Checklist",require_on_completion:"Require on completion",checklist_steps_optional:"Checklist steps (optional)",checklist_placeholder:`Clean filter +/*! maintenance_supporter frontend 2.70.0 */ +var Tt=Object.defineProperty;var cs=Object.getOwnPropertyDescriptor;var w=(r,s,e)=>()=>{if(e)throw e[0];try{return r&&(s=r(r=0)),s}catch(t){throw e=[t],t}};var ds=(r,s)=>{for(var e in s)Tt(r,e,{get:s[e],enumerable:!0})};var c=(r,s,e,t)=>{for(var i=t>1?void 0:t?cs(s,e):s,n=r.length-1,o;n>=0;n--)(o=r[n])&&(i=(t?o(s,e,i):o(i))||i);return t&&i&&Tt(s,e,i),i};var Me,Oe,Xe,Ct,xe,It,S,Pt,et,tt=w(()=>{Me=globalThis,Oe=Me.ShadowRoot&&(Me.ShadyCSS===void 0||Me.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Xe=Symbol(),Ct=new WeakMap,xe=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Xe)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=s,this.t=e}get styleSheet(){let s=this.o,e=this.t;if(Oe&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=Ct.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&Ct.set(e,s))}return s}toString(){return this.cssText}},It=r=>new xe(typeof r=="string"?r:r+"",void 0,Xe),S=(r,...s)=>{let e=r.length===1?r[0]:s.reduce((t,i,n)=>t+(o=>{if(o._$cssResult$===!0)return o.cssText;if(typeof o=="number")return o;throw Error("Value passed to 'css' function must be a 'css' function result: "+o+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+r[n+1],r[0]);return new xe(e,r,Xe)},Pt=(r,s)=>{if(Oe)r.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=Me.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,r.appendChild(t)}},et=Oe?r=>r:r=>r instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return It(e)})(r):r});var ps,us,hs,_s,gs,ms,ze,Lt,fs,vs,we,$e,Fe,qt,Y,ke=w(()=>{tt();tt();({is:ps,defineProperty:us,getOwnPropertyDescriptor:hs,getOwnPropertyNames:_s,getOwnPropertySymbols:gs,getPrototypeOf:ms}=Object),ze=globalThis,Lt=ze.trustedTypes,fs=Lt?Lt.emptyScript:"",vs=ze.reactiveElementPolyfillSupport,we=(r,s)=>r,$e={toAttribute(r,s){switch(s){case Boolean:r=r?fs:null;break;case Object:case Array:r=r==null?r:JSON.stringify(r)}return r},fromAttribute(r,s){let e=r;switch(s){case Boolean:e=r!==null;break;case Number:e=r===null?null:Number(r);break;case Object:case Array:try{e=JSON.parse(r)}catch{e=null}}return e}},Fe=(r,s)=>!ps(r,s),qt={attribute:!0,type:String,converter:$e,reflect:!1,useDefault:!1,hasChanged:Fe};Symbol.metadata??=Symbol("metadata"),ze.litPropertyMetadata??=new WeakMap;Y=class extends HTMLElement{static addInitializer(s){this._$Ei(),(this.l??=[]).push(s)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(s,e=qt){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(s)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(s,e),!e.noAccessor){let t=Symbol(),i=this.getPropertyDescriptor(s,t,e);i!==void 0&&us(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:n}=hs(this.prototype,s)??{get(){return this[e]},set(o){this[e]=o}};return{get:i,set(o){let p=i?.call(this);n?.call(this,o),this.requestUpdate(s,p,t)},configurable:!0,enumerable:!0}}static getPropertyOptions(s){return this.elementProperties.get(s)??qt}static _$Ei(){if(this.hasOwnProperty(we("elementProperties")))return;let s=ms(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(we("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(we("properties"))){let e=this.properties,t=[..._s(e),...gs(e)];for(let i of t)this.createProperty(i,e[i])}let s=this[Symbol.metadata];if(s!==null){let e=litPropertyMetadata.get(s);if(e!==void 0)for(let[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(let[e,t]of this.elementProperties){let i=this._$Eu(e,t);i!==void 0&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(s){let e=[];if(Array.isArray(s)){let t=new Set(s.flat(1/0).reverse());for(let i of t)e.unshift(et(i))}else s!==void 0&&e.push(et(s));return e}static _$Eu(s,e){let t=e.attribute;return t===!1?void 0:typeof t=="string"?t:typeof s=="string"?s.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(s=>this.enableUpdating=s),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(s=>s(this))}addController(s){(this._$EO??=new Set).add(s),this.renderRoot!==void 0&&this.isConnected&&s.hostConnected?.()}removeController(s){this._$EO?.delete(s)}_$E_(){let s=new Map,e=this.constructor.elementProperties;for(let t of e.keys())this.hasOwnProperty(t)&&(s.set(t,this[t]),delete this[t]);s.size>0&&(this._$Ep=s)}createRenderRoot(){let s=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return Pt(s,this.constructor.elementStyles),s}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(s=>s.hostConnected?.())}enableUpdating(s){}disconnectedCallback(){this._$EO?.forEach(s=>s.hostDisconnected?.())}attributeChangedCallback(s,e,t){this._$AK(s,t)}_$ET(s,e){let t=this.constructor.elementProperties.get(s),i=this.constructor._$Eu(s,t);if(i!==void 0&&t.reflect===!0){let n=(t.converter?.toAttribute!==void 0?t.converter:$e).toAttribute(e,t.type);this._$Em=s,n==null?this.removeAttribute(i):this.setAttribute(i,n),this._$Em=null}}_$AK(s,e){let t=this.constructor,i=t._$Eh.get(s);if(i!==void 0&&this._$Em!==i){let n=t.getPropertyOptions(i),o=typeof n.converter=="function"?{fromAttribute:n.converter}:n.converter?.fromAttribute!==void 0?n.converter:$e;this._$Em=i;let p=o.fromAttribute(e,n.type);this[i]=p??this._$Ej?.get(i)??p,this._$Em=null}}requestUpdate(s,e,t,i=!1,n){if(s!==void 0){let o=this.constructor;if(i===!1&&(n=this[s]),t??=o.getPropertyOptions(s),!((t.hasChanged??Fe)(n,e)||t.useDefault&&t.reflect&&n===this._$Ej?.get(s)&&!this.hasAttribute(o._$Eu(s,t))))return;this.C(s,e,t)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(s,e,{useDefault:t,reflect:i,wrapped:n},o){t&&!(this._$Ej??=new Map).has(s)&&(this._$Ej.set(s,o??e??this[s]),n!==!0||o!==void 0)||(this._$AL.has(s)||(this.hasUpdated||t||(e=void 0),this._$AL.set(s,e)),i===!0&&this._$Em!==s&&(this._$Eq??=new Set).add(s))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let s=this.scheduleUpdate();return s!=null&&await s,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[i,n]of this._$Ep)this[i]=n;this._$Ep=void 0}let t=this.constructor.elementProperties;if(t.size>0)for(let[i,n]of t){let{wrapped:o}=n,p=this[i];o!==!0||this._$AL.has(i)||p===void 0||this.C(i,void 0,n,p)}}let s=!1,e=this._$AL;try{s=this.shouldUpdate(e),s?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(t){throw s=!1,this._$EM(),t}s&&this._$AE(e)}willUpdate(s){}_$AE(s){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(s)),this.updated(s)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(s){return!0}update(s){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(s){}firstUpdated(s){}};Y.elementStyles=[],Y.shadowRootOptions={mode:"open"},Y[we("elementProperties")]=new Map,Y[we("finalized")]=new Map,vs?.({ReactiveElement:Y}),(ze.reactiveElementVersions??=[]).push("2.1.2")});function Bt(r,s){if(!ct(r)||!r.hasOwnProperty("raw"))throw Error("invalid template strings array");return Nt!==void 0?Nt.createHTML(s):s}function ue(r,s,e=r,t){if(s===oe)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,n=Ae(s)?void 0:s._$litDirective$;return i?.constructor!==n&&(i?._$AO?.(!1),n===void 0?i=void 0:(i=new n(r),i._$AT(r,e,t)),t!==void 0?(e._$Co??=[])[t]=i:e._$Cl=i),i!==void 0&&(s=ue(r,i._$AS(r,s.values),i,t)),s}var lt,Rt,Ue,Nt,zt,ie,Ft,ys,ne,Se,Ae,ct,bs,it,Ee,jt,Ht,re,Dt,Mt,Ut,dt,l,_e,Ar,oe,_,Ot,ae,xs,Te,st,Ce,he,rt,at,nt,ot,ws,Wt,Be=w(()=>{lt=globalThis,Rt=r=>r,Ue=lt.trustedTypes,Nt=Ue?Ue.createPolicy("lit-html",{createHTML:r=>r}):void 0,zt="$lit$",ie=`lit$${Math.random().toFixed(9).slice(2)}$`,Ft="?"+ie,ys=`<${Ft}>`,ne=document,Se=()=>ne.createComment(""),Ae=r=>r===null||typeof r!="object"&&typeof r!="function",ct=Array.isArray,bs=r=>ct(r)||typeof r?.[Symbol.iterator]=="function",it=`[ +\f\r]`,Ee=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,jt=/-->/g,Ht=/>/g,re=RegExp(`>|${it}(?:([^\\s"'>=/]+)(${it}*=${it}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`,"g"),Dt=/'/g,Mt=/"/g,Ut=/^(?:script|style|textarea|title)$/i,dt=r=>(s,...e)=>({_$litType$:r,strings:s,values:e}),l=dt(1),_e=dt(2),Ar=dt(3),oe=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),Ot=new WeakMap,ae=ne.createTreeWalker(ne,129);xs=(r,s)=>{let e=r.length-1,t=[],i,n=s===2?"":s===3?"":"",o=Ee;for(let p=0;p"?(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]==='"'?Mt:Dt):o===Mt||o===Dt?o=re:o===jt||o===Ht?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)+zt+d.slice(f)+ie+y):d+ie+(f===-2?p:y)}return[Bt(r,n+(r[e]||"")+(s===2?"":s===3?"":"")),t]},Te=class r{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let n=0,o=0,p=s.length-1,d=this.parts,[u,m]=xs(s,e);if(this.el=r.createElement(u,t),ae.currentNode=this.el.content,e===2||e===3){let f=this.el.content.firstChild;f.replaceWith(...f.childNodes)}for(;(i=ae.nextNode())!==null&&d.length0){i.textContent=Ue?Ue.emptyScript:"";for(let y=0;y2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=_}_$AI(s,e=this,t,i){let n=this.strings,o=!1;if(n===void 0)s=ue(this,s,e,0),o=!Ae(s)||s!==this._$AH&&s!==oe,o&&(this._$AH=s);else{let p=s,d,u;for(s=n[0],d=0;d{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let n=e?.renderBefore??null;t._$litPart$=i=new Ce(s.insertBefore(Se(),n),n,void 0,e??{})}return i._$AI(r),i}});var pt,A,$s,Vt=w(()=>{ke();ke();Be();Be();pt=globalThis,A=class extends Y{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let s=super.createRenderRoot();return this.renderOptions.renderBefore??=s.firstChild,s}update(s){let e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(s),this._$Do=Wt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return oe}};A._$litElement$=!0,A.finalized=!0,pt.litElementHydrateSupport?.({LitElement:A});$s=pt.litElementPolyfillSupport;$s?.({LitElement:A});(pt.litElementVersions??=[]).push("4.2.2")});var Kt=w(()=>{});var P=w(()=>{ke();Be();Vt();Kt()});var Gt=w(()=>{});function x(r){return(s,e)=>typeof e=="object"?Es(r,s,e):((t,i,n)=>{let o=i.hasOwnProperty(n);return i.constructor.createProperty(n,t),o?Object.getOwnPropertyDescriptor(i,n):void 0})(r,s,e)}var ks,Es,ut=w(()=>{ke();ks={attribute:!0,type:String,converter:$e,reflect:!1,hasChanged:Fe},Es=(r=ks,s,e)=>{let{kind:t,metadata:i}=e,n=globalThis.litPropertyMetadata.get(i);if(n===void 0&&globalThis.litPropertyMetadata.set(i,n=new Map),t==="setter"&&((r=Object.create(r)).wrapped=!0),n.set(e.name,r),t==="accessor"){let{name:o}=e;return{set(p){let d=s.get.call(this);s.set.call(this,p),this.requestUpdate(o,d,r,!0,p)},init(p){return p!==void 0&&this.C(o,void 0,r,p),p}}}if(t==="setter"){let{name:o}=e;return function(p){let d=this[o];s.call(this,p),this.requestUpdate(o,d,r,!0,p)}}throw Error("Unsupported decorator location: "+t)}});function h(r){return x({...r,state:!0,attribute:!1})}var Yt=w(()=>{ut();});var Qt=w(()=>{});var ge=w(()=>{});var Jt=w(()=>{ge();});var Zt=w(()=>{ge();});var Xt=w(()=>{ge();});var ei=w(()=>{ge();});var ti=w(()=>{ge();});var W=w(()=>{Gt();ut();Yt();Qt();Jt();Zt();Xt();ei();ti()});function ii(r,s){return!r||r<=0?0:r*(Ss[s||"days"]??1)}var Ss,si=w(()=>{"use strict";Ss={days:1,weeks:7,months:30.4368,years:365.25}});function le(r){let s=r.getFullYear(),e=String(r.getMonth()+1).padStart(2,"0"),t=String(r.getDate()).padStart(2,"0");return`${s}-${e}-${t}`}function As(r,s){let e=[];for(let t=0;te.cost).filter(e=>typeof e=="number");return s.length===0?null:s.reduce((e,t)=>e+t,0)/s.length}function Cs(r){let{windowStart:s,windowEnd:e,task:t,entryId:i,objectName:n}=r,o=[],p=(f,v)=>({date:f,entry_id:i,task_id:t.id,task_name:t.name,object_name:n,status:v&&(t.status==="overdue"||t.status==="triggered")?"ok":t.status,days_until_due:v?null:t.days_until_due??null,projected:v,schedule_type:t.schedule_type,interval_days:t.interval_days??null,interval_unit:t.interval_unit??null,responsible_user_id:t.responsible_user_id??null,avg_cost:Ts(t.history),adaptive_enabled:!!t.adaptive_config?.enabled,prediction_confidence:t.threshold_prediction_confidence??null}),d=Math.max(1,Math.round(ii(t.interval_days,t.interval_unit)));if(t.status==="overdue"||t.status==="triggered"){if(o.push(p(s,!1)),t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=Ve(s,d),v=1;for(;f<=e&&v=s&&m<=e)o.push(p(m,!1));else if(m>e)return o;if(t.schedule_type==="time_based"&&t.interval_days&&t.interval_days>0){let f=Ve(m,d),v=o.length;for(;f<=e&&v=s&&(o.push(p(f,!0)),v++),f=Ve(f,d)}return o}function ni(r,s,e,t=null){let i=As(s,e),n=i[0],o=i[i.length-1],p=[];for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t||y.enabled===!1)continue;let k=Cs({windowStart:n,windowEnd:o,task:y,entryId:f,objectName:m});p.push(...k)}}let d=new Map;for(let u of i)d.set(u,[]);for(let u of p){let m=d.get(u.date);m&&m.push(u)}for(let[,u]of d)u.sort((m,f)=>{let v=ai[m.status]??99,y=ai[f.status]??99;if(v!==y)return v-y;if(m.projected!==f.projected)return m.projected?1:-1;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:d.get(u)??[]}))}function Ps(r,s){let e=[];for(let t=s-1;t>=0;t--){let i=new Date(r);i.setDate(i.getDate()-t),i.setHours(0,0,0,0),e.push(le(i))}return e}function oi(r,s,e,t=null){let i=Ps(s,e),n=i[0],o=i[i.length-1],p=new Map;for(let u of i)p.set(u,[]);for(let u of r){let m=u.object?.name||"",f=u.entry_id,v=u.tasks||[];for(let y of v){if(t&&y.responsible_user_id!==t)continue;let k=y.history||[];for(let E of k){if(typeof E?.timestamp!="string")continue;let H=E.timestamp.slice(0,10);if(Ho)continue;let M=p.get(H);if(!M)continue;let q=E.type??"completed";M.push({date:H,entry_id:f,task_id:y.id,task_name:y.name,object_name:m,status:Is[q]??"ok",days_until_due:null,projected:!1,schedule_type:y.schedule_type,interval_days:y.interval_days??null,responsible_user_id:y.responsible_user_id??null,avg_cost:typeof E.cost=="number"?E.cost:null,adaptive_enabled:!!y.adaptive_config?.enabled,prediction_confidence:null,history_timestamp:E.timestamp,history_type:q,history_cost:typeof E.cost=="number"?E.cost:null,history_notes:typeof E.notes=="string"?E.notes:null,history_duration:typeof E.duration=="number"?E.duration:null})}}}let d={completed:0,reset:1,skipped:2,triggered:3,trigger_replaced:4};for(let[,u]of p)u.sort((m,f)=>{let v=d[m.history_type??""]??99,y=d[f.history_type??""]??99;if(v!==y)return v-y;let k=m.object_name.localeCompare(f.object_name);return k!==0?k:m.task_name.localeCompare(f.task_name)});return i.map(u=>({date:u,events:p.get(u)??[]}))}var ri,ai,Is,ht=w(()=>{"use strict";si();ri=5;ai={overdue:0,triggered:1,due_soon:2,ok:3};Is={completed:"ok",reset:"ok",skipped:"due_soon",missed:"overdue",triggered:"triggered",trigger_replaced:"triggered",trigger_removed:"ok"}});var di,ci=w(()=>{di={maintenance:"Maintenance",objects:"Objects",tasks:"Tasks",overdue:"Overdue",due_soon:"Due Soon",triggered:"Triggered",trigger_replaced:"Trigger replaced",trigger_removed:"Trigger removed",ok:"OK",all:"All",new_object:"+ New Object",templates_from:"From template",templates_title:"Start from a template",templates_task_count:"{n} tasks",template_created:"Created from template",onboard_hint:"Add your first object to start tracking maintenance.",edit:"Edit",duplicate:"Duplicate",task_duplicated:"Task duplicated",object_duplicated:"Object duplicated",delete:"Delete",add_task:"+ Add Task",complete:"Complete",completed:"Completed",skip:"Skip",skipped:"Skipped",missed:"Missed",reset:"Reset",snooze:"Snooze",snoozed:"Snoozed",cancel:"Cancel",bulk_select:"Select",bulk_select_all:"Select all",bulk_n_selected:"{n} selected",bulk_completed:"{n} tasks completed",bulk_archived:"{n} tasks archived",completing:"Completing\u2026",interval:"Interval",warning:"Warning",last_performed:"Last performed",next_due:"Next due",days_until_due:"Days until due",avg_duration:"Avg duration",trigger:"Trigger",trigger_type:"Trigger type",threshold_above:"Upper limit",threshold_below:"Lower limit",threshold:"Threshold",counter:"Counter",state_change:"State change",runtime:"Runtime",runtime_hours:"Target runtime (hours)",target_value:"Target value",baseline:"Baseline",target_changes:"Target changes",for_minutes:"For (minutes)",time_based:"Time-based",sensor_based:"Sensor-based",manual:"Manual",one_time:"One-time",weekdays:"Weekdays",nth_weekday:"Nth weekday of month",day_of_month:"Day of month",recurrence_on_days:"Repeat on",recurrence_occurrence:"Occurrence",recurrence_weekday:"Weekday",recurrence_day:"Day of month (1\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N days, e.g. -2 = two days before.",last_day_month:"Last day of month",last_business_day_month:"Last business day",ord_1:"1st",ord_2:"2nd",ord_3:"3rd",ord_4:"4th",ord_5:"5th",ord_last:"Last",day_word:"Day",interval_value:"Interval",interval_unit:"Unit",unit_days:"Days",unit_weeks:"Weeks",unit_months:"Months",unit_years:"Years",due_date:"Due date",cleaning:"Cleaning",inspection:"Inspection",replacement:"Replacement",calibration:"Calibration",service:"Service",reading:"Reading",custom:"Custom",history:"History",cost:"Cost",report_button:"Report",report_title:"Maintenance report",report_generated:"Generated",report_times_done:"Done",report_total_cost:"Total cost",report_every:"every {n} {unit}",report_notes:"Notes",report_col_type:"Type",report_col_status:"Status",report_col_schedule:"Schedule",duration:"Duration",both:"Both",trigger_val:"Trigger value",complete_title:"Complete: ",checklist:"Checklist",require_on_completion:"Require on completion",checklist_steps_optional:"Checklist steps (optional)",checklist_placeholder:`Clean filter Replace seal -Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"{field}: too long (max {n} characters)",err_too_short:"{field}: too short (min {n} characters)",err_value_too_high:"{field}: too large (max {n})",err_value_too_low:"{field}: too small (min {n})",err_required:"{field}: required",err_wrong_type:"{field}: wrong type (expected: {type})",err_invalid_choice:"{field}: not an allowed value",err_invalid_value:"{field}: invalid value",feat_schedule_time:"Time-of-day scheduling",feat_schedule_time_desc:"Tasks become overdue at a specific time of day instead of midnight.",schedule_time_optional:"Due at time (optional, HH:MM)",schedule_time_help:"Empty = midnight (default). HA timezone.",at_time:"at",notes_optional:"Notes (optional)",notes_markdown_hint:"Markdown is supported \u2014 **bold**, lists, [links](\u2026)",cost_optional:"Cost (optional)",duration_minutes:"Duration in minutes (optional)",completed_at_optional:"Completed at (optional, empty = now)",completed_at_future_error:"The completion date cannot be in the future.",days:"days",day:"day",today:"Today",d_overdue:"d overdue",no_tasks:"No maintenance tasks yet. Create an object to get started.",no_tasks_short:"No tasks",no_history:"No history entries yet.",show_all:"Show all",cost_duration_chart:"Cost & Duration",installed:"Installed",confirm_delete_object:"Delete this object and all its tasks?",confirm_delete_task:"Delete this task?",min:"Min",max:"Max",save:"Save",saving:"Saving\u2026",edit_task:"Edit Task",new_task:"New Maintenance Task",task_name:"Task name",maintenance_type:"Maintenance type",priority:"Priority",labels:"Labels",labels_placeholder:"e.g. safety, seasonal, tenant-visible",labels_help:"Comma-separated tags for filtering and reporting.",priority_low:"Low",priority_normal:"Normal",priority_high:"High",all_priorities:"All priorities",schedule_type:"Schedule type",interval_days:"Interval (days)",warning_days:"Warning days",earliest_completion_days:"Earliest completion (days before due)",earliest_completion_days_help:"Leave empty to allow completing any time. 0 = only on/after the due date.",last_performed_optional:"Last performed (optional)",interval_anchor:"Interval anchor",anchor_completion:"From completion date",anchor_planned:"From planned date (no drift)",edit_object:"Edit Object",name:"Name",manufacturer_optional:"Manufacturer (optional)",model_optional:"Model (optional)",serial_number_optional:"Serial number (optional)",serial_number_label:"S/N",documentation_url_label:"Manual",object_notes_label:"Notes",sort_due_date:"Due date",sort_object:"Object name",sort_type:"Type",sort_task_name:"Task name",all_objects:"All objects",all_parts:"All parts",tasks_lower:"tasks",no_tasks_yet:"No tasks yet",add_first_task:"Add first task",trigger_configuration:"Trigger Configuration",entity_id:"Entity ID",comma_separated:"comma-separated",entity_logic:"Entity logic",entity_logic_any:"Any entity triggers",entity_logic_all:"All entities must trigger",entities:"entities",attribute_optional:"Attribute (optional, blank = state)",use_entity_state:"Use entity state (no attribute)",trigger_above:"Trigger above",trigger_below:"Trigger below",trigger_equals:"Trigger when equal to (=)",trigger_not_equals:"Trigger when different from (\u2260)",for_at_least_minutes:"For at least (minutes)",safety_interval_days:"Safety interval (days, optional)",safety_interval:"Safety interval (optional)",trigger_combinator:"Combine trigger and interval",trigger_combinator_any:"Trigger or interval (whichever first)",trigger_combinator_all:"Trigger and interval (both required)",delta_mode:"Delta mode",from_state_optional:"From state (optional)",to_state_optional:"To state (optional)",documentation_url_optional:"Documentation URL (optional)",object_notes_optional:"Notes (optional)",nfc_tag_id_optional:"NFC Tag ID (optional)",nfc_tags_empty_help:"No NFC tags registered in Home Assistant yet.",nfc_tags_open_settings:"Open Tags settings",nfc_tags_refresh:"Refresh",environmental_entity_optional:"Environmental sensor (optional)",environmental_entity_helper:"e.g. sensor.outdoor_temperature \u2014 adjusts the interval based on environmental conditions",adaptive_prediction_enabled:"Enable sensor-driven predictions",adaptive_seasonal_enabled:"Enable seasonal awareness",adaptive_max_interval:"Maximum interval (days)",adaptive_min_interval:"Minimum interval (days)",adaptive_ewa_alpha:"Learning rate (alpha)",adaptive_enabled:"Enable adaptive scheduling",adaptive_section_title:"Adaptive Scheduling",environmental_attribute_optional:"Environmental attribute (optional)",nfc_tag_id:"NFC Tag ID",nfc_linked:"NFC tag linked",nfc_link_hint:"Click to link NFC tag",responsible_user:"Responsible User",shared_with:"Shared with (rotation)",shared_with_help:"Pick multiple people to share this task; the responsible person rotates on each completion.",rotation_strategy:"Rotation",rotation_none:"No rotation",rotation_round_robin:"Round-robin",rotation_least_completed:"Least completed",rotation_random:"Random",no_user_assigned:"(No user assigned)",all_users:"All Users",my_tasks:"My Tasks",tab_calendar:"Calendar",cal_no_events:"No maintenance",cal_window_7:"7 days",cal_window_14:"14 days",cal_window_30:"30 days",cal_window_365:"1 year",cal_every_n_days:"every {n} days",cal_source_time:"Time-based",cal_source_time_adaptive:"Time-based (adaptive)",cal_source_sensor:"Sensor-based",cal_predicted:"predicted",cal_confidence_high:"high confidence",cal_confidence_medium:"medium confidence",cal_confidence_low:"low confidence",budget_monthly:"Monthly budget",budget_yearly:"Yearly budget",groups:"Groups",new_group:"New group",edit_group:"Edit group",no_groups:"No groups yet",delete_group:"Delete group",delete_group_confirm:"Delete group '{name}'?",group_select_tasks:"Select tasks",group_name_required:"Name is required",description_optional:"Description (optional)",selected:"Selected",loading_chart:"Loading chart data...",hide_outliers:"Hide outliers (sensor glitches)",was_maintenance_needed:"Was this maintenance needed?",feedback_needed:"Needed",feedback_not_needed:"Not needed",feedback_not_sure:"Not sure",suggested_interval:"Suggested interval",apply_suggestion:"Apply",reanalyze:"Re-analyze",reanalyze_result:"New analysis",reanalyze_insufficient_data:"Not enough data to produce a recommendation",data_points:"data points",dismiss_suggestion:"Dismiss",confidence_low:"Low",confidence_medium:"Medium",confidence_high:"High",recommended:"recommended",seasonal_awareness:"Seasonal Awareness",edit_seasonal_overrides:"Edit seasonal factors",seasonal_overrides_title:"Seasonal factors (override)",seasonal_overrides_hint:"Factor per month (0.1\u20135.0). Empty = learned automatically.",seasonal_override_invalid:"Invalid value",seasonal_override_range:"Factor must be between 0.1 and 5.0",clear_all:"Clear all",seasonal_chart_title:"Seasonal Factors",seasonal_learned:"Learned",seasonal_manual:"Manual",month_jan:"Jan",month_feb:"Feb",month_mar:"Mar",month_apr:"Apr",month_may:"May",month_jun:"Jun",month_jul:"Jul",month_aug:"Aug",month_sep:"Sep",month_oct:"Oct",month_nov:"Nov",month_dec:"Dec",sensor_prediction:"Sensor Prediction",degradation_trend:"Trend",trend_rising:"Rising",trend_falling:"Falling",trend_stable:"Stable",trend_insufficient_data:"Insufficient data",days_until_threshold:"Days until threshold",threshold_exceeded:"Threshold exceeded",environmental_adjustment:"Environmental factor",sensor_prediction_urgency:"Sensor predicts threshold in ~{days} days",day_short:"day",weibull_reliability_curve:"Reliability Curve",weibull_failure_probability:"Failure Probability",weibull_r_squared:"Fit R\xB2",beta_early_failures:"Early Failures",beta_random_failures:"Random Failures",beta_wear_out:"Wear-out",beta_highly_predictable:"Highly Predictable",confidence_interval:"Confidence Interval",confidence_conservative:"Conservative",confidence_aggressive:"Optimistic",current_interval_marker:"Current interval",recommended_marker:"Recommended",characteristic_life:"Characteristic life",chart_mini_sparkline:"Trend sparkline",chart_history:"Cost and duration history",chart_seasonal:"Seasonal factors, 12 months",chart_weibull:"Weibull reliability curve",chart_sparkline:"Sensor trigger value chart",days_progress:"Days progress",qr_code:"QR Code",qr_generating:"Generating QR code\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 Network.",save_error:"Failed to save. Please try again.",qr_print:"Print",qr_download:"Download SVG",qr_action:"Action on scan",qr_action_view:"View maintenance info",qr_action_complete:"Mark maintenance as complete",qr_url_mode:"Link type",qr_mode_companion:"Companion App",qr_mode_local:"Local (mDNS)",qr_mode_server:"Server URL",overview:"Overview",analysis:"Analysis",recent_activities:"Recent Activities",search_notes:"Search notes",avg_cost:"Avg Cost",no_advanced_features:"No advanced features enabled",no_advanced_features_hint:"Enable \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D in the integration settings to see analysis data here.",analysis_not_enough_data:"Not enough data for analysis yet.",analysis_not_enough_data_hint:"Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.",analysis_manual_task_hint:"Manual tasks without an interval do not generate analysis data.",completions:"completions",current:"Current",shorter:"Shorter",longer:"Longer",normal:"Normal",disabled:"Disabled",compound_logic:"Compound logic",compound:"Compound (multiple conditions)",compound_logic_and:"AND \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 add at least one.",compound_add_condition:"Add condition",compound_condition:"Condition",compound_remove_condition:"Remove condition",card_title:"Title",card_show_header:"Show header with statistics",card_show_actions:"Show action buttons",card_compact:"Compact mode",card_max_items:"Max items (0 = all)",card_filter_status:"Filter by status",card_filter_status_help:"Empty = show all statuses.",card_filter_objects:"Filter by objects",card_filter_objects_help:"Empty = show all objects.",card_filter_areas:"Filter by areas",card_filter_areas_help:"Empty = show all areas.",card_filter_priority_help:"Empty = show all priorities. Tasks without an explicit priority count as Normal.",card_filter_entities:"Filter by entities (entity_ids)",card_filter_entities_help:"Pick sensor / binary_sensor entities from this integration. Empty = all.",card_loading_objects:"Loading objects\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 Create one in the Maintenance panel",no_objects:"No objects yet.",action_error:"Action failed. Please try again.",area_id_optional:"Area (optional)",installation_date_optional:"Installation date (optional)",warranty_expiry_optional:"Warranty expiry (optional)",warranty:"Warranty",warranty_valid_until:"valid until {date}",warranty_expires_in:"expires in {days} days",warranty_expired:"expired",cal_past_windows:"Past windows",cal_forward_windows:"Forward windows",history_edit_title:"Edit history entry",history_edit_timestamp:"Timestamp",manufacturer:"Manufacturer",model:"Model",area:"Area",actions:"Actions",view_mode_label:"View",view_cards:"Card view",view_table:"Table view",objects_table_columns_label:"Objects table columns",objects_table_columns_hint:"Choose which columns appear in the objects table view.",custom_icon_optional:"Icon (optional, e.g. mdi:wrench)",task_enabled:"Task enabled",skip_reason_prompt:"Skip this task?",reason_optional:"Reason (optional)",reset_date_prompt:"Mark task as performed?",reset_date_optional:"Last performed date (optional, defaults to today)",notes_label:"Notes",documentation_label:"Documentation",no_nfc_tag:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 Esc to close",today_all_caught_up:"All caught up! Nothing due this week.",today_overdue:"Overdue",today_due_today:"Due today",today_this_week:"This week",settings:"Settings",settings_features:"Advanced Features",settings_features_desc:"Enable or disable advanced features. Disabling hides them from the UI but does not delete data.",feat_adaptive:"Adaptive Scheduling",feat_adaptive_desc:"Learn optimal intervals from maintenance history",feat_predictions:"Sensor Predictions",feat_predictions_desc:"Predict trigger dates from sensor degradation",feat_seasonal:"Seasonal Adjustments",feat_seasonal_desc:"Adjust intervals based on seasonal patterns",feat_environmental:"Environmental Correlation",feat_environmental_desc:"Correlate intervals with temperature/humidity",feat_budget:"Budget Tracking",feat_budget_desc:"Track monthly and yearly maintenance spending",feat_groups:"Task Groups",feat_groups_desc:"Organize tasks into logical groups",feat_checklists:"Checklists",feat_checklists_desc:"Multi-step procedures for task completion",settings_general:"General",settings_default_warning:"Default warning days",settings_panel_enabled:"Sidebar panel",settings_panel_title:"Sidebar panel title",settings_notifications:"Notifications",settings_notify_service:"Notification service",settings_shopping_list:"Shopping list (buy tasks)",settings_shopping_list_help:"Low-part buy reminders appear in this to-do list; checking one off restocks the part.",shopping_list_none:"Off \u2014 no shopping list",settings_install_assist_sentences:"Install Assist sentences",settings_install_assist_sentences_hint:"Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",notify_per_person:"Per-person delivery",notify_no_own_device:"No own device \u2014 uses the household service",settings_notify_due_soon:"Notify when due soon",settings_notify_overdue:"Notify when overdue",settings_notify_triggered:"Notify when triggered",settings_interval_hours:"Repeat interval (hours, 0 = once)",settings_quiet_hours:"Quiet hours",settings_quiet_start:"Start",settings_quiet_end:"End",settings_max_per_day:"Max notifications per day (0 = unlimited)",settings_bundling:"Bundle notifications",settings_bundle_threshold:"Bundle threshold",settings_reminder_leads:"Extra reminders (days before due)",settings_reminder_leads_hint:"Comma-separated lead times, e.g. 14, 3, 0 \u2014 one extra reminder fires on each matching day. Empty = off.",settings_actions:"Mobile Action Buttons",settings_action_complete:"Show 'Complete' button",settings_action_skip:"Show 'Skip' button",settings_action_snooze:"Show 'Snooze' button",settings_weekly_digest:"Weekly digest",settings_weekly_digest_hint:"A single summary notification on Monday morning when tasks are due.",settings_warranty_reminder:"Warranty expiry reminder",settings_warranty_reminder_days:"Days before expiry",settings_warranty_reminder_hint:"Notify once when an object's warranty is this many days from expiring.",settings_snooze_hours:"Snooze duration (hours)",settings_budget:"Budget",settings_currency:"Currency",settings_budget_monthly:"Monthly budget",settings_budget_yearly:"Yearly budget",settings_budget_alerts:"Budget alerts",settings_budget_threshold:"Alert threshold (%)",settings_import_export:"Import / Export",settings_export_json:"Export JSON",settings_export_yaml:"Export YAML",settings_export_csv:"Export CSV",settings_export_settings:"Export settings (JSON)",settings_import_csv:"Import CSV",settings_import_placeholder:"Paste JSON or CSV content here\u2026",settings_import_btn:"Import",settings_import_success:"{count} objects imported successfully.",settings_export_success:"Export downloaded.",settings_saved:"Setting saved.",settings_include_history:"Include history",settings_export_selection:"Limit to selected objects (optional)",settings_docs_archive:"Documents archive (with files)",settings_docs_archive_hint:"The JSON/YAML/CSV exports carry settings only. This ZIP includes the uploaded file contents so a restore is complete.",settings_docs_export_btn:"Download documents ZIP",settings_docs_import_btn:"Restore documents ZIP",settings_docs_import_success:"Restored: {blobs} files, {docs} documents",sort_alphabetical:"Alphabetical",sort_due_soonest:"Due soonest",sort_task_count:"Task count",sort_area:"Area",sort_assigned_user:"Assigned user",sort_group:"Group",groupby_none:"No grouping",groupby_area:"By area",groupby_group:"By group",groupby_user:"By user",filter_label:"Filter",user_label:"User",photo_label:"Photo",sort_label:"Sort",group_by_label:"Group by",state_value_help:'Use the HA state value (usually lowercase, e.g. "on"/"off"). Case is normalised on save.',target_changes_help:"Number of matching transitions before the trigger fires (default: 1).",for_minutes_state_help:"0 counts every change immediately. Set minutes and the new state must hold that long first \u2014 brief flickers then neither trigger nor count.",qr_print_title:"Print QR codes",qr_print_desc:"Generate a printable page of QR codes to cut out and stick on your equipment.",qr_print_load:"Load objects",qr_print_filter:"Filter",qr_print_objects:"Objects",qr_print_actions:"Actions",qr_print_url_mode:"Link type",qr_print_estimate:"Estimated QR codes",qr_print_over_limit:"cap is 200, narrow the filter",qr_print_generate:"Generate QR codes",qr_print_generating:"Generating\u2026",qr_print_ready:"QR codes ready",qr_print_print_button:"Print",qr_print_empty:"Nothing to generate",qr_action_skip:"Skip",vacation_title:"Vacation mode",vacation_active:"active",vacation_ended:"ended",vacation_desc:"Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.",vacation_enable:"Enable vacation mode",vacation_start:"Start",vacation_end:"End",vacation_buffer:"Buffer (days)",vacation_exempt_title:"Notify anyway during vacation",vacation_exempt_desc:"Pick tasks that should still notify during vacation (e.g. critical pool chemistry).",vacation_load_tasks:"Load tasks",vacation_preview_btn:"Show preview",vacation_preview_affected:"tasks affected",vacation_event_due_soon:"becomes due soon",vacation_event_overdue:"becomes overdue",vacation_event_triggered_est:"sensor trigger possible",vacation_sensor_based:"(sensor-based)",vacation_action_notify:"Notify anyway",vacation_action_unsilence:"Silence again",vacation_marked_complete:"Marked complete",vacation_marked_skip:"Skipped",vacation_end_now:"End vacation now",add:"Add",show_stats:"Show stats + graphs",hide_stats:"Hide stats",adaptive_no_data:"Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.",suggestion_applied:"Suggested interval applied",vacation_mode:"Vacation mode",vacation_status_active:"Active now",vacation_status_scheduled:"Scheduled",vacation_status_inactive:"Inactive",vacation_end_now_confirm:"End vacation immediately?",vacation_exempt_count:"exempt",vacation_advanced:"Advanced\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",groups_open_panel:"Open in panel",unassigned:"Unassigned",no_area:"No area",has_overdue:"Has overdue tasks",object:"Object",settings_panel_access:"Panel access",settings_panel_access_desc:"Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below \u2014 everyone else sees only Complete and Skip.",settings_operator_write:"Allow selected users to create, edit & delete",settings_operator_write_desc:"Off: only admins can change content. On: the selected users below get full access too.",no_non_admin_users:"No non-admin users found. Add some in Settings \u2192 People.",owner_label:"Owner",feat_completion_actions:"Completion actions",feat_completion_actions_desc:"Per-task HA action on complete + quick-complete QR with pre-set values.",on_complete_action_title:"On complete: trigger HA action (optional)",on_complete_action_desc:"Calls an HA service when the task is completed \u2014 e.g. reset a counter on the device.",on_complete_action_service:"Service",on_complete_action_target:"Target entity",on_complete_action_target_hint:"Note: the entity domain must match the service \u2014 e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').",on_complete_action_data:"Data (JSON, optional)",on_complete_action_test:"Validate configuration",on_complete_action_test_success:"\u2713 Configuration valid (action will fire only on task completion)",on_complete_action_test_failed:"Failed",quick_complete_defaults_title:"Quick-complete defaults (for QR scans, optional)",quick_complete_defaults_desc:"Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.",quick_complete_defaults_notes:"Notes",quick_complete_defaults_cost:"Cost",quick_complete_defaults_duration:"Duration (minutes)",quick_complete_defaults_feedback_none:"No feedback",quick_complete_defaults_feedback_needed:"Was needed",quick_complete_defaults_feedback_not_needed:"Not needed",quick_complete_success:"Quickly marked complete",show_all_objects:"Show all objects",show_all_tasks:"Clear filter \u2014 show all tasks",filter_to_overdue:"Filter task list to overdue only",filter_to_due_soon:"Filter task list to due-soon only",filter_to_triggered:"Filter task list to triggered only",open_task:"Open task",show_details:"Show history + stats",hide_details:"Hide details",history_empty:"No history yet.",history_edit_button:"Edit entry",total_cost:"Total cost",times_performed:"Performed",older_entries:"older",open_in_panel:"Open in Maintenance panel",skip_reason:"Skip reason (optional)",reset_to_date:"Reset last_performed to",delete_task_confirm:"Delete this task and its history?",delete_object_confirm:"Delete this object and all its tasks?",loading:"Loading\u2026",archive:"Archive",undo:"Undo",task_archived:"Task archived",object_archived:"Object archived",unarchive:"Unarchive",archived:"Archived",show_archived:"Show archived",hide_archived:"Hide archived",confirm_archive_object:"Archive this object and its tasks? They keep their history and can be unarchived later.",settings_archive:"Archive & Retention",settings_archive_desc:"Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.",settings_archive_oneoff_days:"Auto-archive completed one-off tasks after (days, 0 = off)",settings_delete_archived_oneoff_days:"Auto-delete archived one-off tasks after (days, 0 = never)",archive_object:"Archive object",unarchive_object:"Unarchive object",documents:"Documents",documents_empty:"No documents yet.",doc_upload:"Upload file",doc_uploading:"Uploading\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",doc_link_title:"Title (optional)",doc_open:"Open",doc_delete_confirm:'Delete "{name}"?',doc_too_large:"File is too large (max 25 MB).",doc_upload_failed:"Upload failed.",completion_photo_optional:"Completion photo (optional)",add_photo:"Add photo",uploading:"Uploading\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 shared, no extra space used.",doc_dup_in_object:"This file is already attached to this object.",doc_link_invalid:"Only http/https links are allowed.",doc_cat_manual:"Manual",doc_cat_warranty:"Warranty",doc_cat_invoice:"Invoice",doc_cat_spare_parts:"Spare parts",doc_cat_photo:"Photo",doc_cat_other:"Other",doc_link_badge:"Link",doc_storage_title:"Document storage",doc_storage_saved:"Saved via deduplication",doc_storage_refresh:"Refresh",doc_download:"Download",doc_close:"Close",doc_camera:"Take photo",doc_drop_hint:"Drop files here",doc_task_none:"No documents linked to this task.",doc_link_existing:"Link a document\u2026",doc_attach:"Link",doc_unlink:"Unlink",doc_page:"Page",chart_range_7d:"7d",chart_range_30d:"30d",chart_range_90d:"90d",chart_range_1y:"1y",chart_since_service:"since last service",chart_no_stats:"No long-term statistics for this entity \u2014 showing maintenance-event values only",auto_complete_on_recovery:"Auto-complete when the sensor recovers",auto_complete_on_recovery_help:"Records a completion (sets last performed) when the trigger clears itself \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",doc_search_none:"No matching documents",link_device_optional:"Link to existing device (optional)",parent_object_optional:"Parent object (optional)",parent_none:"(No parent)",paused:"Paused",pause_object:"Pause",resume_object:"Resume",pause_until_prompt:"Freeze this object's schedules \u2014 nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.",pause_until_label:"Resume on (optional)",object_paused:"Object paused",object_resumed:"Object resumed \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",replace_object_prompt:"Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.",replace_name_label:"Successor name",object_replaced:"Object replaced \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",reading_unit_help:"Shown next to the recorded value when completing this task.",reading_value_label:"Reading value",reading_label:"Reading",settings_templates_label:"Template gallery",settings_templates_hint:`Untick templates you'll never need \u2014 they disappear from the "From template" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.`,worksheet:"Work sheet",worksheet_scan_view:"Scan to open the task",worksheet_scan_complete:"Scan to complete",worksheet_manual_excerpt:"Manual excerpt",worksheet_pages:"pages",worksheet_printed:"Printed",worksheet_never:"Never",card_all_caught_up:"All caught up \u2014 nothing needs attention",postpone:"Postpone",postpone_date_prompt:"Postpone this occurrence to which date?",postpone_date_label:"New due date",postponed:"Postponed",postponed_to:"Postponed to",season_window_label:"Seasonal window (months)",season_window_hint:"Only due in the selected months; off-season dates roll to the next active month. None = all year.",series_end_label:"Ends",series_end_never:"Never (repeats indefinitely)",series_end_after_count:"After a number of times",series_end_until:"On a date",series_end_count_label:"Number of times",series_end_until_label:"End date",parts_section:"Parts & consumables",parts_inventory_value:"Inventory value",part_add:"Add part",part_name:"Name",part_vendor:"Manufacturer",part_storage_location:"Storage location",part_product_url:"Product URL",part_unit:"Unit",part_cost:"Unit price",part_stock:"Stock",part_reorder_threshold:"Reorder at",part_restock_quantity:"Restock quantity",part_auto_buy:"Auto-create buy task when low",part_restock:"Adjust stock",parts_used_by:"Used by",restock_quantity_label:"Quantity bought",consumes_parts_label:"Consumes parts",shared_parts_other_objects:"Parts from other objects",shared_parts_help:"Several objects can share one stock. Completing this task takes from the owning object.",shared_part_unknown:"Unknown part",parts_load_failed:"Couldn't load this object's parts \u2014 the consumes-parts options are unavailable right now.",adopt_problem_button:"Adopt problem sensors",adopt_problem_title:"Adopt problem sensors",adopt_problem_hint:"Turn HA problem sensors (printer errors, filter warnings, low battery) into maintenance tasks that trigger while the problem is active and clear themselves when it resolves.",adopt_problem_none:"No problem sensors found that aren't already tracked.",adopt_problem_active:"active",adopt_problem_ok:"ok",adopt_problem_new_object:"(new)",adopt_problem_adopt:"Adopt selected",adopt_problem_done:"Adopted {tasks} problem sensor(s)",views_label:"Views",views_none:"\u2014 No view \u2014",views_manage:"Save / manage views",views_dialog_title:"Saved views",views_dialog_hint:"Save the current filters as a named view everyone can reuse.",views_name_placeholder:"View name",views_save_current:"Save current filters",views_none_yet:"No saved views yet.",close:"Close",trigger_hint_now:"The sensor reads {value} right now.",trigger_hint_above:"The task triggers once it rises above {target}.",trigger_hint_below:"It triggers once it falls below {target}.",trigger_hint_counter_delta:"Counts from the current reading ({value}): due at {due} (+{target}), and the count restarts after each completion.",trigger_hint_counter_delta_edit:"Counts usage since the last completion: due after +{target}; the count restarts after each completion.",trigger_hint_counter_abs:"The task becomes due once the sensor reaches {target}.",trigger_hint_runtime:"The task becomes due after {hours} h of accumulated on-time; the counter restarts after each completion.",trigger_hint_state_change:"The task becomes due after {count} state change(s).",trigger_hint_state_change_to:"The task becomes due after {count} change(s) to \u201C{state}\u201D.",trigger_hint_state_now:"Current state: {value}.",adopt_problem_part:"Uses part: {name}",label_filter:"Label",all_labels:"All labels",settings_notify_scope:"Notify only for view",settings_notify_scope_all:"All tasks",settings_notify_scope_hint:"Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",card_saved_view:"Saved view",card_saved_view_none:"None",card_saved_view_help:"Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",doc_part_none:"No documents linked to this part.",settings_templates_toggle_group:"Enable or disable all templates in this group",setups_button:"Suggested setups",setups_title:"Suggested setups (Beta)",setups_hint:"Devices of supported integrations whose consumable sensors can drive maintenance tasks. Adopting creates the object and wires each task to its sensor \u2014 it triggers when the consumable runs low and resolves itself after replacement.",setups_none:"No supported devices with unwired consumable sensors found.",setups_adopt:"Set up selected",setups_done:"{tasks} sensor-wired tasks created.",complete_parts_used:"Parts used this time",part_delete_confirm:"Delete part '{name}'? Its stock tracking, task links and any open buy reminder will be removed.",baseline_start_value:"Start reading (optional)",baseline_start_help:"Counting starts from this reading. Leave empty to count from the current value; enter the reading at the last service so usage since then already counts.",setups_baseline_hint:"reading at last service (optional)",baseline_start_help_edit:"Leave empty to keep the existing counting. Entering a value re-anchors the counting (e.g. the reading at the last service).",baseline_current_effective:"Currently effective start value: {value}",runtime_on_states:"Active states",runtime_on_states_help:"States that count as running \u2014 default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",setups_target_new:"Create new: {name}",schedule_preview_title:"Next dates",schedule_preview_ontime:"Assuming on-time completion.",schedule_preview_ends:"(series ends)",adopt_problem_responsible:"Responsible user for all adopted tasks (optional)",adopt_for_minutes_hint:"Only trigger once the problem has persisted this long \u2014 0 reacts to the first flicker.",adopt_problem_configure:"Configure",history_auto:"Automatic",battery_fleet_title:"Battery fleet",battery_fleet_none_low:"All batteries OK \u2014 nothing to replace.",battery_fleet_buy_now:"Buy now",battery_fleet_soon:"Needed soon",battery_fleet_soon_hint:"Predicted from the last replacement date \u2014 order ahead.",battery_fleet_mark_all:"Mark all replaced",battery_fleet_mark_one:"Mark this battery replaced",battery_fleet_offline:"offline",battery_fleet_trigger_lost:"This task's sensor trigger was lost \u2014 it will not fire or auto-complete.",battery_fleet_repair:"Repair",battery_fleet_exclude:"Exclude from the fleet",battery_fleet_excluded:"Excluded",battery_fleet_include:"Track again",battery_fleet_all:"All tracked batteries",battery_fleet_all_hint:"Exclude a device here to drop it from the fleet before it ever reports low \u2014 a vacuum that recharges itself, or a phone that warns you on its own.",battery_fleet_add:"Add a battery",battery_fleet_add_hint:"Pick a battery sensor the automatic discovery missed \u2014 it joins the roster immediately.",battery_fleet_track_self:"Track self-charging batteries",battery_fleet_track_self_hint:"Phones, vacuums and other devices that recharge themselves appear as rechargeables \u2014 a low one asks for a charge, never for new cells.",battery_fleet_status_low:"Low",battery_fleet_status_soon:"Soon",battery_fleet_status_ok:"Healthy",battery_fleet_predicted_on:"Expected around {date}",battery_fleet_predicted_trend:"Predicted from this battery's discharge trend: around {date} ({confidence})",battery_fleet_rechargeable:"Rechargeable: charge instead of replacing \u2014 never on the shopping list",battery_fleet_sort_name:"Sort by name",battery_fleet_sort_urgency:"Sort by urgency",battery_fleet_mark_recharged:"Mark as recharged",battery_fleet_sparkline_hint:"Battery level over the last 30 days \u2014 dotted: projected until the low threshold",battery_fleet_filter_type:"Show only this battery type",battery_fleet_record_replacement:"The level jumped around {date} \u2014 record this replacement in Battery Notes",battery_fleet_total:"{n} batteries tracked",battery_fleet_setup_button:"Battery fleet",battery_fleet_setup_done:"Battery fleet set up \u2014 one task tracks all your batteries.",update_banner:"A newer version of Maintenance Supporter is on the server \u2014 reload to update the panel.",update_reload:"Reload",battery_fleet_forecast_overdue:"Predicted date passed \u2014 the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",cost_from_parts:"Use \u2248 {amount} from parts",dismiss:"Dismiss",gs_label:"Getting started \u2014 these hints retire as your setup grows",gs_setups_chip:"Suggested setups found {n} devices with pre-wired triggers",gs_adopt_chip:"{n} problem sensors can become maintenance tasks",gs_fleet_chip:"One click sets up the battery fleet",cal_editor_window:"Default window",cal_editor_window_week:"Week (7 days)",cal_editor_window_fortnight:"Fortnight (14 days)",cal_editor_window_month:"Month (30 days, default)",cal_editor_window_year:"Year (365 days, empty days collapsed)",cal_editor_show_chips:"Show window chips inside the card",cal_editor_chips_hint:"Hide the chips when the card is embedded in a strategy view that already serves as the window selector.",cal_editor_show_user_filter:"Show user filter dropdown",cal_editor_default_user:"Default user filter",cal_editor_my_tasks:"My tasks (current user)",cal_editor_show_object_filter:"Show object filter dropdown",cal_editor_object_hint:'Pre-select one object via YAML: object_filter: "" \u2014 or a list of names to restrict the card to several objects.',object_history_section:"History (all tasks)",object_history_all_tasks:"All tasks",object_history_empty:"No entries in this range.",object_history_cap_note:"History keeps up to 500 entries per task \u2014 very old entries may be missing.",service_record_title:"Service record",service_record_print:"Service record (PDF)",date:"Date",service_record_entries:"entries",completed_by:"Completed by",date_from:"From",date_to:"To",phases_section:"Cycle phases (optional)",phases_hint:"Different work on one shared schedule \u2014 each completion moves to the next step (e.g. small service, small service, big service).",phase_add:"Add phase",phase_name:"Phase name",phase_sequence_label:"Cycle order",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 \u2014 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 \u201CRequire on completion\u201D 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 \u2014 saving here will be refused."}});var ci,di=w(()=>{"use strict";ci="2.68.0"});var Ce,pi=w(()=>{"use strict";Ce={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"}});function qs(r){ce.en=Object.assign({},r,ce.en??{})}function qe(r){let s=(r||_t).toLowerCase();return s.startsWith("pt")&&s.endsWith("br")?"pt-br":s.substring(0,2)}function a(r,s){let e=qe(s);return ce[e]?.[r]??ce.en[r]??r}function _i(r,s){s.has("hass")&&Hs(r.hass?.locale,r.hass?.config?.country);let e=r.hass?.language;e&&!Re(e)&&je(e).then(()=>r.requestUpdate())}function D(r){return r?.language||"en"}function Re(r){let s=qe(r);return s===_t||s in ce}function je(r){let s=qe(r);return s===_t||s in ce||!Rs.has(s)?Promise.resolve():(s in Pe||(Pe[s]=fetch(`${js}/${s}.json?v=${ci}`).then(e=>e.ok?e.json():null).then(e=>{e?ce[s]=e:delete Pe[s]}).catch(()=>{delete Pe[s]})),Pe[s])}function gi(r){let s=qe(r);return{de:"de-DE",en:"en-US",nl:"nl-NL",fr:"fr-FR",it:"it-IT",es:"es-ES",pt:"pt-PT",ru:"ru-RU",uk:"uk-UA",zh:"zh-CN",da:"da-DK",fi:"fi-FI",nb:"nb-NO",ja:"ja-JP",hi:"hi-IN",pl:"pl-PL",cs:"cs-CZ",sv:"sv-SE","pt-br":"pt-BR",hu:"hu-HU",ko:"ko-KR",tr:"tr-TR"}[s]??"en-US"}function Hs(r,s){r&&(me.date=r.date_format,me.time=r.time_format,s!==void 0&&(me.country=s||void 0))}function Ve(r){let s=gi(r),e=me.country;if(e&&/^[A-Za-z]{2}$/.test(e)){let t=`${qe(r).split("-")[0]}-${e.toUpperCase()}`;try{return new Intl.DateTimeFormat(t),t}catch{}}return s}function mi(r,s){let e=String(r.getDate()).padStart(2,"0"),t=String(r.getMonth()+1).padStart(2,"0"),i=String(r.getFullYear());switch(me.date){case"DMY":return`${e}/${t}/${i}`;case"MDY":return`${t}/${e}/${i}`;case"YMD":return`${i}-${t}-${e}`;case"system":return r.toLocaleDateString(void 0,{day:"2-digit",month:"2-digit",year:"numeric"});default:return r.toLocaleDateString(Ve(s),{day:"2-digit",month:"2-digit",year:"numeric"})}}function Ds(r,s){switch(me.time){case"12":return r.toLocaleTimeString(Ve(s),{hour:"2-digit",minute:"2-digit",hour12:!0});case"24":return r.toLocaleTimeString(Ve(s),{hour:"2-digit",minute:"2-digit",hour12:!1});case"system":return r.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"});default:return r.toLocaleTimeString(Ve(s),{hour:"2-digit",minute:"2-digit"})}}function Q(r,s){if(!r)return"\u2014";try{let e=r.includes("T")?r:r+"T00:00:00";return mi(new Date(e),s)}catch{return r}}function fi(r,s){if(!r)return"\u2014";try{let e=new Date(r);return mi(e,s)+" "+Ds(e,s)}catch{return r}}function vi(r,s){if(r==null)return"\u2014";let e=s||"en";return r<0?`${Math.abs(r)} ${a("d_overdue",e)}`:r===0?a("today",e):`${r} ${a(r===1?"day":"days",e)}`}function Ke(r,s,e){return r==null?"\u2014":`${r} ${a("unit_"+(s||"days"),e)}`}function Le(r,s,e="long"){return new Date(Date.UTC(2024,0,1+r)).toLocaleDateString(gi(s),{weekday:e,timeZone:"UTC"})}function yi(r,s){let e=r.schedule,t=e?.offset?` ${e.offset>0?"+":"\u2212"}${Math.abs(e.offset)}d`:"";switch(e?.kind){case"weekdays":return((e.weekdays||[]).map(i=>Le(i,s,"short")).join(" & ")||"\u2014")+t;case"nth_weekday":return e.weekday==null||e.nth==null?"\u2014":`${e.nth===-1?a("ord_last",s):a("ord_"+e.nth,s)} ${Le(e.weekday,s,"long")}${t}`;case"day_of_month":return e.day==null?"\u2014":(e.day===-1?a(e.business?"last_business_day_month":"last_day_month",s):`${a("day_word",s)} ${e.day}`)+t;case"one_time":return r.due_date?Q(r.due_date,s):a("one_time",s);case"manual":return a("manual",s);case"interval":return Ke(e.every,e.unit,s)}return r.schedule_type==="one_time"?r.due_date?Q(r.due_date,s):a("one_time",s):r.schedule_type==="manual"?a("manual",s):r.schedule_type==="sensor_based"?a("sensor_based",s):r.interval_days!=null?Ke(r.interval_days,r.interval_unit,s):"\u2014"}function bi(r,s){r.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:s},bubbles:!0,composed:!0}))}var ui,_t,hi,ce,Rs,js,Pe,Ns,me,xi,Ge,j=w(()=>{"use strict";P();oi();di();pi();ui="\u20AC",_t="en",hi=(()=>{let r=window;return r.__msLocales||(r.__msLocales={store:{},inflight:{}}),r.__msLocales})(),ce=hi.store;qs(li);Rs=new Set(["de","nl","fr","it","es","pt","pt-br","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi","hu","ko","tr"]),js="/maintenance_supporter_locales",Pe=hi.inflight;Ns=window,me=Ns.__msDateTimePrefs??={};xi=S` +Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"{field}: too long (max {n} characters)",err_too_short:"{field}: too short (min {n} characters)",err_value_too_high:"{field}: too large (max {n})",err_value_too_low:"{field}: too small (min {n})",err_required:"{field}: required",err_wrong_type:"{field}: wrong type (expected: {type})",err_invalid_choice:"{field}: not an allowed value",err_invalid_value:"{field}: invalid value",feat_schedule_time:"Time-of-day scheduling",feat_schedule_time_desc:"Tasks become overdue at a specific time of day instead of midnight.",schedule_time_optional:"Due at time (optional, HH:MM)",schedule_time_help:"Empty = midnight (default). HA timezone.",at_time:"at",notes_optional:"Notes (optional)",notes_markdown_hint:"Markdown is supported \u2014 **bold**, lists, [links](\u2026)",cost_optional:"Cost (optional)",duration_minutes:"Duration in minutes (optional)",completed_at_optional:"Completed at (optional, empty = now)",completed_at_future_error:"The completion date cannot be in the future.",days:"days",day:"day",today:"Today",d_overdue:"d overdue",no_tasks:"No maintenance tasks yet. Create an object to get started.",no_tasks_short:"No tasks",no_history:"No history entries yet.",show_all:"Show all",cost_duration_chart:"Cost & Duration",installed:"Installed",confirm_delete_object:"Delete this object and all its tasks?",confirm_delete_task:"Delete this task?",min:"Min",max:"Max",save:"Save",saving:"Saving\u2026",edit_task:"Edit Task",new_task:"New Maintenance Task",task_name:"Task name",maintenance_type:"Maintenance type",priority:"Priority",labels:"Labels",labels_placeholder:"e.g. safety, seasonal, tenant-visible",labels_help:"Comma-separated tags for filtering and reporting.",priority_low:"Low",priority_normal:"Normal",priority_high:"High",all_priorities:"All priorities",schedule_type:"Schedule type",interval_days:"Interval (days)",warning_days:"Warning days",earliest_completion_days:"Earliest completion (days before due)",earliest_completion_days_help:"Leave empty to allow completing any time. 0 = only on/after the due date.",last_performed_optional:"Last performed (optional)",interval_anchor:"Interval anchor",anchor_completion:"From completion date",anchor_planned:"From planned date (no drift)",edit_object:"Edit Object",name:"Name",manufacturer_optional:"Manufacturer (optional)",model_optional:"Model (optional)",serial_number_optional:"Serial number (optional)",serial_number_label:"S/N",documentation_url_label:"Manual",object_notes_label:"Notes",sort_due_date:"Due date",sort_object:"Object name",sort_type:"Type",sort_task_name:"Task name",all_objects:"All objects",all_parts:"All parts",tasks_lower:"tasks",no_tasks_yet:"No tasks yet",add_first_task:"Add first task",trigger_configuration:"Trigger Configuration",entity_id:"Entity ID",comma_separated:"comma-separated",entity_logic:"Entity logic",entity_logic_any:"Any entity triggers",entity_logic_all:"All entities must trigger",entities:"entities",attribute_optional:"Attribute (optional, blank = state)",use_entity_state:"Use entity state (no attribute)",trigger_above:"Trigger above",trigger_below:"Trigger below",trigger_equals:"Trigger when equal to (=)",trigger_not_equals:"Trigger when different from (\u2260)",for_at_least_minutes:"For at least (minutes)",safety_interval_days:"Safety interval (days, optional)",safety_interval:"Safety interval (optional)",trigger_combinator:"Combine trigger and interval",trigger_combinator_any:"Trigger or interval (whichever first)",trigger_combinator_all:"Trigger and interval (both required)",delta_mode:"Delta mode",from_state_optional:"From state (optional)",to_state_optional:"To state (optional)",documentation_url_optional:"Documentation URL (optional)",object_notes_optional:"Notes (optional)",nfc_tag_id_optional:"NFC Tag ID (optional)",nfc_tags_empty_help:"No NFC tags registered in Home Assistant yet.",nfc_tags_open_settings:"Open Tags settings",nfc_tags_refresh:"Refresh",environmental_entity_optional:"Environmental sensor (optional)",environmental_entity_helper:"e.g. sensor.outdoor_temperature \u2014 adjusts the interval based on environmental conditions",adaptive_prediction_enabled:"Enable sensor-driven predictions",adaptive_seasonal_enabled:"Enable seasonal awareness",adaptive_max_interval:"Maximum interval (days)",adaptive_min_interval:"Minimum interval (days)",adaptive_ewa_alpha:"Learning rate (alpha)",adaptive_enabled:"Enable adaptive scheduling",adaptive_section_title:"Adaptive Scheduling",environmental_attribute_optional:"Environmental attribute (optional)",nfc_tag_id:"NFC Tag ID",nfc_linked:"NFC tag linked",nfc_link_hint:"Click to link NFC tag",responsible_user:"Responsible User",shared_with:"Shared with (rotation)",shared_with_help:"Pick multiple people to share this task; the responsible person rotates on each completion.",rotation_strategy:"Rotation",rotation_none:"No rotation",rotation_round_robin:"Round-robin",rotation_least_completed:"Least completed",rotation_random:"Random",no_user_assigned:"(No user assigned)",all_users:"All Users",my_tasks:"My Tasks",tab_calendar:"Calendar",cal_no_events:"No maintenance",cal_window_7:"7 days",cal_window_14:"14 days",cal_window_30:"30 days",cal_window_365:"1 year",cal_every_n_days:"every {n} days",cal_source_time:"Time-based",cal_source_time_adaptive:"Time-based (adaptive)",cal_source_sensor:"Sensor-based",cal_predicted:"predicted",cal_confidence_high:"high confidence",cal_confidence_medium:"medium confidence",cal_confidence_low:"low confidence",budget_monthly:"Monthly budget",budget_yearly:"Yearly budget",groups:"Groups",new_group:"New group",edit_group:"Edit group",no_groups:"No groups yet",delete_group:"Delete group",delete_group_confirm:"Delete group '{name}'?",group_select_tasks:"Select tasks",group_name_required:"Name is required",description_optional:"Description (optional)",selected:"Selected",loading_chart:"Loading chart data...",hide_outliers:"Hide outliers (sensor glitches)",was_maintenance_needed:"Was this maintenance needed?",feedback_needed:"Needed",feedback_not_needed:"Not needed",feedback_not_sure:"Not sure",suggested_interval:"Suggested interval",apply_suggestion:"Apply",reanalyze:"Re-analyze",reanalyze_result:"New analysis",reanalyze_insufficient_data:"Not enough data to produce a recommendation",data_points:"data points",dismiss_suggestion:"Dismiss",confidence_low:"Low",confidence_medium:"Medium",confidence_high:"High",recommended:"recommended",seasonal_awareness:"Seasonal Awareness",edit_seasonal_overrides:"Edit seasonal factors",seasonal_overrides_title:"Seasonal factors (override)",seasonal_overrides_hint:"Factor per month (0.1\u20135.0). Empty = learned automatically.",seasonal_override_invalid:"Invalid value",seasonal_override_range:"Factor must be between 0.1 and 5.0",clear_all:"Clear all",seasonal_chart_title:"Seasonal Factors",seasonal_learned:"Learned",seasonal_manual:"Manual",month_jan:"Jan",month_feb:"Feb",month_mar:"Mar",month_apr:"Apr",month_may:"May",month_jun:"Jun",month_jul:"Jul",month_aug:"Aug",month_sep:"Sep",month_oct:"Oct",month_nov:"Nov",month_dec:"Dec",sensor_prediction:"Sensor Prediction",degradation_trend:"Trend",trend_rising:"Rising",trend_falling:"Falling",trend_stable:"Stable",trend_insufficient_data:"Insufficient data",days_until_threshold:"Days until threshold",threshold_exceeded:"Threshold exceeded",environmental_adjustment:"Environmental factor",sensor_prediction_urgency:"Sensor predicts threshold in ~{days} days",day_short:"day",weibull_reliability_curve:"Reliability Curve",weibull_failure_probability:"Failure Probability",weibull_r_squared:"Fit R\xB2",beta_early_failures:"Early Failures",beta_random_failures:"Random Failures",beta_wear_out:"Wear-out",beta_highly_predictable:"Highly Predictable",confidence_interval:"Confidence Interval",confidence_conservative:"Conservative",confidence_aggressive:"Optimistic",current_interval_marker:"Current interval",recommended_marker:"Recommended",characteristic_life:"Characteristic life",chart_mini_sparkline:"Trend sparkline",chart_history:"Cost and duration history",chart_seasonal:"Seasonal factors, 12 months",chart_weibull:"Weibull reliability curve",chart_sparkline:"Sensor trigger value chart",days_progress:"Days progress",qr_code:"QR Code",qr_generating:"Generating QR code\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 Network.",save_error:"Failed to save. Please try again.",qr_print:"Print",qr_download:"Download SVG",qr_action:"Action on scan",qr_action_view:"View maintenance info",qr_action_complete:"Mark maintenance as complete",qr_url_mode:"Link type",qr_mode_companion:"Companion App",qr_mode_local:"Local (mDNS)",qr_mode_server:"Server URL",overview:"Overview",analysis:"Analysis",recent_activities:"Recent Activities",search_notes:"Search notes",avg_cost:"Avg Cost",no_advanced_features:"No advanced features enabled",no_advanced_features_hint:"Enable \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D in the integration settings to see analysis data here.",analysis_not_enough_data:"Not enough data for analysis yet.",analysis_not_enough_data_hint:"Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.",analysis_manual_task_hint:"Manual tasks without an interval do not generate analysis data.",completions:"completions",current:"Current",shorter:"Shorter",longer:"Longer",normal:"Normal",disabled:"Disabled",compound_logic:"Compound logic",compound:"Compound (multiple conditions)",compound_logic_and:"AND \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 add at least one.",compound_add_condition:"Add condition",compound_condition:"Condition",compound_remove_condition:"Remove condition",card_title:"Title",card_show_header:"Show header with statistics",card_show_actions:"Show action buttons",card_action_style:"Complete button style",card_compact:"Compact mode",card_max_items:"Max items (0 = all)",card_filter_status:"Filter by status",card_filter_status_help:"Empty = show all statuses.",card_filter_objects:"Filter by objects",card_filter_objects_help:"Empty = show all objects.",card_filter_areas:"Filter by areas",card_filter_areas_help:"Empty = show all areas.",card_filter_priority_help:"Empty = show all priorities. Tasks without an explicit priority count as Normal.",card_filter_entities:"Filter by entities (entity_ids)",card_filter_entities_help:"Pick sensor / binary_sensor entities from this integration. Empty = all.",card_loading_objects:"Loading objects\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 Create one in the Maintenance panel",no_objects:"No objects yet.",action_error:"Action failed. Please try again.",area_id_optional:"Area (optional)",installation_date_optional:"Installation date (optional)",warranty_expiry_optional:"Warranty expiry (optional)",warranty:"Warranty",warranty_valid_until:"valid until {date}",warranty_expires_in:"expires in {days} days",warranty_expired:"expired",cal_past_windows:"Past windows",cal_forward_windows:"Forward windows",history_edit_title:"Edit history entry",history_edit_timestamp:"Timestamp",manufacturer:"Manufacturer",model:"Model",area:"Area",actions:"Actions",view_mode_label:"View",view_cards:"Card view",view_table:"Table view",objects_table_columns_label:"Objects table columns",objects_table_columns_hint:"Choose which columns appear in the objects table view.",custom_icon_optional:"Icon (optional, e.g. mdi:wrench)",task_enabled:"Task enabled",skip_reason_prompt:"Skip this task?",reason_optional:"Reason (optional)",reset_date_prompt:"Mark task as performed?",reset_date_optional:"Last performed date (optional, defaults to today)",notes_label:"Notes",documentation_label:"Documentation",no_nfc_tag:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 Esc to close",today_all_caught_up:"All caught up! Nothing due this week.",today_overdue:"Overdue",today_due_today:"Due today",today_this_week:"This week",settings:"Settings",settings_features:"Advanced Features",settings_features_desc:"Enable or disable advanced features. Disabling hides them from the UI but does not delete data.",feat_adaptive:"Adaptive Scheduling",feat_adaptive_desc:"Learn optimal intervals from maintenance history",feat_predictions:"Sensor Predictions",feat_predictions_desc:"Predict trigger dates from sensor degradation",feat_seasonal:"Seasonal Adjustments",feat_seasonal_desc:"Adjust intervals based on seasonal patterns",feat_environmental:"Environmental Correlation",feat_environmental_desc:"Correlate intervals with temperature/humidity",feat_budget:"Budget Tracking",feat_budget_desc:"Track monthly and yearly maintenance spending",feat_groups:"Task Groups",feat_groups_desc:"Organize tasks into logical groups",feat_checklists:"Checklists",feat_checklists_desc:"Multi-step procedures for task completion",settings_general:"General",settings_default_warning:"Default warning days",settings_consumable_threshold:"Consumable low threshold (%)",settings_battery_low_percent:"Battery low threshold (%)",settings_thresholds_hint:"Defaults for new Suggested setups (percent-remaining consumables) and for fleet batteries without their own Battery Notes threshold. Existing tasks keep their value.",bn_summary:"{name}: {pct} % for {n} batteries",bn_floor_decides:"Your {floor} % floor decides for all of them; the higher of the two thresholds counts per battery.",bn_above_floor:"That is above your {floor} % floor, so {name} decides for all noted batteries.",bn_overrides:"{n} devices with their own threshold:",bn_more:"+ {n} more",settings_row_actions:"Task row actions",row_actions_buttons_compact:"Buttons (icons only on phones)",row_actions_buttons:"Buttons with text",row_actions_icons:"Icons (classic)",row_actions_follow:"Follow the household setting",settings_panel_enabled:"Sidebar panel",settings_panel_title:"Sidebar panel title",settings_notifications:"Notifications",settings_notify_service:"Notification service",settings_shopping_list:"Shopping list (buy tasks)",settings_shopping_list_help:"Low-part buy reminders appear in this to-do list; checking one off restocks the part.",shopping_list_none:"Off \u2014 no shopping list",settings_install_assist_sentences:"Install Assist sentences",settings_install_assist_sentences_hint:"Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",notify_per_person:"Per-person delivery",notify_no_own_device:"No own device \u2014 uses the household service",settings_notify_due_soon:"Notify when due soon",settings_notify_overdue:"Notify when overdue",settings_notify_triggered:"Notify when triggered",settings_interval_hours:"Repeat interval (hours, 0 = once)",settings_quiet_hours:"Quiet hours",settings_quiet_start:"Start",settings_quiet_end:"End",settings_max_per_day:"Max notifications per day (0 = unlimited)",settings_bundling:"Bundle notifications",settings_bundle_threshold:"Bundle threshold",settings_reminder_leads:"Extra reminders (days before due)",settings_reminder_leads_hint:"Comma-separated lead times, e.g. 14, 3, 0 \u2014 one extra reminder fires on each matching day. Empty = off.",settings_actions:"Mobile Action Buttons",settings_action_complete:"Show 'Complete' button",settings_action_skip:"Show 'Skip' button",settings_action_snooze:"Show 'Snooze' button",settings_weekly_digest:"Weekly digest",settings_weekly_digest_hint:"A single summary notification on Monday morning when tasks are due.",settings_warranty_reminder:"Warranty expiry reminder",settings_warranty_reminder_days:"Days before expiry",settings_warranty_reminder_hint:"Notify once when an object's warranty is this many days from expiring.",settings_snooze_hours:"Snooze duration (hours)",settings_budget:"Budget",settings_currency:"Currency",settings_budget_monthly:"Monthly budget",settings_budget_yearly:"Yearly budget",settings_budget_alerts:"Budget alerts",settings_budget_threshold:"Alert threshold (%)",settings_import_export:"Import / Export",settings_export_json:"Export JSON",settings_export_yaml:"Export YAML",settings_export_csv:"Export CSV",settings_export_settings:"Export settings (JSON)",settings_import_csv:"Import CSV",settings_import_placeholder:"Paste JSON or CSV content here\u2026",settings_import_btn:"Import",settings_import_success:"{count} objects imported successfully.",settings_export_success:"Export downloaded.",settings_saved:"Setting saved.",settings_include_history:"Include history",settings_export_selection:"Limit to selected objects (optional)",settings_docs_archive:"Documents archive (with files)",settings_docs_archive_hint:"The JSON/YAML/CSV exports carry settings only. This ZIP includes the uploaded file contents so a restore is complete.",settings_docs_export_btn:"Download documents ZIP",settings_docs_import_btn:"Restore documents ZIP",settings_docs_import_success:"Restored: {blobs} files, {docs} documents",sort_alphabetical:"Alphabetical",sort_due_soonest:"Due soonest",sort_task_count:"Task count",sort_area:"Area",sort_assigned_user:"Assigned user",sort_group:"Group",groupby_none:"No grouping",groupby_area:"By area",groupby_group:"By group",groupby_user:"By user",filter_label:"Filter",user_label:"User",photo_label:"Photo",sort_label:"Sort",group_by_label:"Group by",state_value_help:'Use the HA state value (usually lowercase, e.g. "on"/"off"). Case is normalised on save.',target_changes_help:"Number of matching transitions before the trigger fires (default: 1).",for_minutes_state_help:"0 counts every change immediately. Set minutes and the new state must hold that long first \u2014 brief flickers then neither trigger nor count.",qr_print_title:"Print QR codes",qr_print_desc:"Generate a printable page of QR codes to cut out and stick on your equipment.",qr_print_load:"Load objects",qr_print_filter:"Filter",qr_print_objects:"Objects",qr_print_actions:"Actions",qr_print_url_mode:"Link type",qr_print_estimate:"Estimated QR codes",qr_print_over_limit:"cap is 200, narrow the filter",qr_print_generate:"Generate QR codes",qr_print_generating:"Generating\u2026",qr_print_ready:"QR codes ready",qr_print_print_button:"Print",qr_print_empty:"Nothing to generate",qr_action_skip:"Skip",vacation_title:"Vacation mode",vacation_active:"active",vacation_ended:"ended",vacation_desc:"Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.",vacation_enable:"Enable vacation mode",vacation_start:"Start",vacation_end:"End",vacation_buffer:"Buffer (days)",vacation_exempt_title:"Notify anyway during vacation",vacation_exempt_desc:"Pick tasks that should still notify during vacation (e.g. critical pool chemistry).",vacation_load_tasks:"Load tasks",vacation_preview_btn:"Show preview",vacation_preview_affected:"tasks affected",vacation_event_due_soon:"becomes due soon",vacation_event_overdue:"becomes overdue",vacation_event_triggered_est:"sensor trigger possible",vacation_sensor_based:"(sensor-based)",vacation_action_notify:"Notify anyway",vacation_action_unsilence:"Silence again",vacation_marked_complete:"Marked complete",vacation_marked_skip:"Skipped",vacation_end_now:"End vacation now",add:"Add",show_stats:"Show stats + graphs",hide_stats:"Hide stats",adaptive_no_data:"Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.",suggestion_applied:"Suggested interval applied",vacation_mode:"Vacation mode",vacation_status_active:"Active now",vacation_status_scheduled:"Scheduled",vacation_status_inactive:"Inactive",vacation_end_now_confirm:"End vacation immediately?",vacation_exempt_count:"exempt",vacation_advanced:"Advanced\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",groups_open_panel:"Open in panel",unassigned:"Unassigned",no_area:"No area",has_overdue:"Has overdue tasks",object:"Object",settings_panel_access:"Panel access",settings_panel_access_desc:"Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below \u2014 everyone else sees only Complete and Skip.",settings_operator_write:"Allow selected users to create, edit & delete",settings_operator_write_desc:"Off: only admins can change content. On: the selected users below get full access too.",no_non_admin_users:"No non-admin users found. Add some in Settings \u2192 People.",owner_label:"Owner",feat_completion_actions:"Completion actions",feat_completion_actions_desc:"Per-task HA action on complete + quick-complete QR with pre-set values.",on_complete_action_title:"On complete: trigger HA action (optional)",on_complete_action_desc:"Calls an HA service when the task is completed \u2014 e.g. reset a counter on the device.",on_complete_action_service:"Service",on_complete_action_target:"Target entity",on_complete_action_target_hint:"Note: the entity domain must match the service \u2014 e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').",on_complete_action_data:"Data (JSON, optional)",on_complete_action_test:"Validate configuration",on_complete_action_test_success:"\u2713 Configuration valid (action will fire only on task completion)",on_complete_action_test_failed:"Failed",quick_complete_defaults_title:"Quick-complete defaults (for QR scans, optional)",quick_complete_defaults_desc:"Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.",quick_complete_defaults_notes:"Notes",quick_complete_defaults_cost:"Cost",quick_complete_defaults_duration:"Duration (minutes)",quick_complete_defaults_feedback_none:"No feedback",quick_complete_defaults_feedback_needed:"Was needed",quick_complete_defaults_feedback_not_needed:"Not needed",quick_complete_success:"Quickly marked complete",show_all_objects:"Show all objects",show_all_tasks:"Clear filter \u2014 show all tasks",filter_to_overdue:"Filter task list to overdue only",filter_to_due_soon:"Filter task list to due-soon only",filter_to_triggered:"Filter task list to triggered only",open_task:"Open task",show_details:"Show history + stats",hide_details:"Hide details",history_empty:"No history yet.",history_edit_button:"Edit entry",total_cost:"Total cost",times_performed:"Performed",older_entries:"older",open_in_panel:"Open in Maintenance panel",skip_reason:"Skip reason (optional)",reset_to_date:"Reset last_performed to",delete_task_confirm:"Delete this task and its history?",delete_object_confirm:"Delete this object and all its tasks?",loading:"Loading\u2026",archive:"Archive",undo:"Undo",task_archived:"Task archived",object_archived:"Object archived",unarchive:"Unarchive",archived:"Archived",show_archived:"Show archived",hide_archived:"Hide archived",confirm_archive_object:"Archive this object and its tasks? They keep their history and can be unarchived later.",settings_archive:"Archive & Retention",settings_archive_desc:"Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.",settings_archive_oneoff_days:"Auto-archive completed one-off tasks after (days, 0 = off)",settings_delete_archived_oneoff_days:"Auto-delete archived one-off tasks after (days, 0 = never)",archive_object:"Archive object",unarchive_object:"Unarchive object",documents:"Documents",documents_empty:"No documents yet.",doc_upload:"Upload file",doc_uploading:"Uploading\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",doc_link_title:"Title (optional)",doc_open:"Open",doc_delete_confirm:'Delete "{name}"?',doc_too_large:"File is too large (max 25 MB).",doc_upload_failed:"Upload failed.",completion_photo_optional:"Completion photo (optional)",add_photo:"Add photo",uploading:"Uploading\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 shared, no extra space used.",doc_dup_in_object:"This file is already attached to this object.",doc_link_invalid:"Only http/https links are allowed.",doc_cat_manual:"Manual",doc_cat_warranty:"Warranty",doc_cat_invoice:"Invoice",doc_cat_spare_parts:"Spare parts",doc_cat_photo:"Photo",doc_cat_other:"Other",doc_link_badge:"Link",doc_storage_title:"Document storage",doc_storage_saved:"Saved via deduplication",doc_storage_refresh:"Refresh",doc_download:"Download",doc_close:"Close",doc_camera:"Take photo",doc_drop_hint:"Drop files here",doc_task_none:"No documents linked to this task.",doc_link_existing:"Link a document\u2026",doc_attach:"Link",doc_unlink:"Unlink",doc_page:"Page",chart_range_7d:"7d",chart_range_30d:"30d",chart_range_90d:"90d",chart_range_1y:"1y",chart_since_service:"since last service",chart_no_stats:"No long-term statistics for this entity \u2014 showing maintenance-event values only",auto_complete_on_recovery:"Auto-complete when the sensor recovers",auto_complete_on_recovery_help:"Records a completion (sets last performed) when the trigger clears itself \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",doc_search_none:"No matching documents",link_device_optional:"Link to existing device (optional)",parent_object_optional:"Parent object (optional)",parent_none:"(No parent)",paused:"Paused",pause_object:"Pause",resume_object:"Resume",pause_until_prompt:"Freeze this object's schedules \u2014 nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.",pause_until_label:"Resume on (optional)",object_paused:"Object paused",object_resumed:"Object resumed \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",replace_object_prompt:"Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.",replace_name_label:"Successor name",object_replaced:"Object replaced \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",reading_unit_help:"Shown next to the recorded value when completing this task.",reading_value_label:"Reading value",reading_label:"Reading",settings_templates_label:"Template gallery",settings_templates_hint:`Untick templates you'll never need \u2014 they disappear from the "From template" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.`,worksheet:"Work sheet",worksheet_scan_view:"Scan to open the task",worksheet_scan_complete:"Scan to complete",worksheet_manual_excerpt:"Manual excerpt",worksheet_pages:"pages",worksheet_printed:"Printed",worksheet_never:"Never",card_all_caught_up:"All caught up \u2014 nothing needs attention",postpone:"Postpone",postpone_date_prompt:"Postpone this occurrence to which date?",postpone_date_label:"New due date",postponed:"Postponed",postponed_to:"Postponed to",season_window_label:"Seasonal window (months)",season_window_hint:"Only due in the selected months; off-season dates roll to the next active month. None = all year.",series_end_label:"Ends",series_end_never:"Never (repeats indefinitely)",series_end_after_count:"After a number of times",series_end_until:"On a date",series_end_count_label:"Number of times",series_end_until_label:"End date",parts_section:"Parts & consumables",parts_inventory_value:"Inventory value",part_add:"Add part",part_name:"Name",part_vendor:"Manufacturer",part_storage_location:"Storage location",part_product_url:"Product URL",part_unit:"Unit",part_cost:"Unit price",part_stock:"Stock",part_reorder_threshold:"Reorder at",part_restock_quantity:"Restock quantity",part_auto_buy:"Auto-create buy task when low",part_restock:"Adjust stock",parts_used_by:"Used by",restock_quantity_label:"Quantity bought",consumes_parts_label:"Consumes parts",shared_parts_other_objects:"Parts from other objects",shared_parts_help:"Several objects can share one stock. Completing this task takes from the owning object.",shared_part_unknown:"Unknown part",parts_load_failed:"Couldn't load this object's parts \u2014 the consumes-parts options are unavailable right now.",adopt_problem_button:"Adopt problem sensors",adopt_problem_title:"Adopt problem sensors",adopt_problem_hint:"Turn HA problem sensors (printer errors, filter warnings, low battery) into maintenance tasks that trigger while the problem is active and clear themselves when it resolves.",adopt_problem_none:"No problem sensors found that aren't already tracked.",adopt_problem_active:"active",adopt_problem_ok:"ok",adopt_problem_new_object:"(new)",adopt_problem_adopt:"Adopt selected",adopt_problem_done:"Adopted {tasks} problem sensor(s)",views_label:"Views",views_none:"\u2014 No view \u2014",views_manage:"Save / manage views",views_dialog_title:"Saved views",views_dialog_hint:"Save the current filters as a named view everyone can reuse.",views_name_placeholder:"View name",views_save_current:"Save current filters",views_none_yet:"No saved views yet.",close:"Close",trigger_hint_now:"The sensor reads {value} right now.",trigger_hint_above:"The task triggers once it rises above {target}.",trigger_hint_below:"It triggers once it falls below {target}.",trigger_hint_counter_delta:"Counts from the current reading ({value}): due at {due} (+{target}), and the count restarts after each completion.",trigger_hint_counter_delta_edit:"Counts usage since the last completion: due after +{target}; the count restarts after each completion.",trigger_hint_counter_abs:"The task becomes due once the sensor reaches {target}.",trigger_hint_runtime:"The task becomes due after {hours} h of accumulated on-time; the counter restarts after each completion.",trigger_hint_state_change:"The task becomes due after {count} state change(s).",trigger_hint_state_change_to:"The task becomes due after {count} change(s) to \u201C{state}\u201D.",trigger_hint_state_now:"Current state: {value}.",adopt_problem_part:"Uses part: {name}",label_filter:"Label",all_labels:"All labels",settings_notify_scope:"Notify only for view",settings_notify_scope_all:"All tasks",settings_notify_scope_hint:"Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",card_saved_view:"Saved view",card_saved_view_none:"None",card_saved_view_help:"Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",doc_part_none:"No documents linked to this part.",settings_templates_toggle_group:"Enable or disable all templates in this group",setups_button:"Suggested setups",setups_title:"Suggested setups (Beta)",setups_hint:"Devices of supported integrations whose consumable sensors can drive maintenance tasks. Adopting creates the object and wires each task to its sensor \u2014 it triggers when the consumable runs low and resolves itself after replacement.",setups_none:"No supported devices with unwired consumable sensors found.",setups_adopt:"Set up selected",setups_done:"{tasks} sensor-wired tasks created.",complete_parts_used:"Parts used this time",part_delete_confirm:"Delete part '{name}'? Its stock tracking, task links and any open buy reminder will be removed.",baseline_start_value:"Start reading (optional)",baseline_start_help:"Counting starts from this reading. Leave empty to count from the current value; enter the reading at the last service so usage since then already counts.",setups_baseline_hint:"reading at last service (optional)",baseline_start_help_edit:"Leave empty to keep the existing counting. Entering a value re-anchors the counting (e.g. the reading at the last service).",baseline_current_effective:"Currently effective start value: {value}",runtime_on_states:"Active states",runtime_on_states_help:"States that count as running \u2014 default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",setups_target_new:"Create new: {name}",schedule_preview_title:"Next dates",schedule_preview_ontime:"Assuming on-time completion.",schedule_preview_ends:"(series ends)",adopt_problem_responsible:"Responsible user for all adopted tasks (optional)",adopt_for_minutes_hint:"Only trigger once the problem has persisted this long \u2014 0 reacts to the first flicker.",adopt_problem_configure:"Configure",history_auto:"Automatic",battery_fleet_title:"Battery fleet",battery_fleet_none_low:"All batteries OK \u2014 nothing to replace.",battery_fleet_buy_now:"Buy now",battery_fleet_soon:"Needed soon",battery_fleet_soon_hint:"Predicted from the last replacement date \u2014 order ahead.",battery_fleet_mark_all:"Mark all replaced",battery_fleet_mark_one:"Mark this battery replaced",battery_fleet_offline:"offline",battery_fleet_trigger_lost:"This task's sensor trigger was lost \u2014 it will not fire or auto-complete.",battery_fleet_repair:"Repair",battery_fleet_exclude:"Exclude from the fleet",battery_fleet_excluded:"Excluded",battery_fleet_include:"Track again",battery_fleet_all:"All tracked batteries",battery_fleet_all_hint:"Exclude a device here to drop it from the fleet before it ever reports low \u2014 a vacuum that recharges itself, or a phone that warns you on its own.",battery_fleet_add:"Add a battery",battery_fleet_add_hint:"Pick a battery sensor the automatic discovery missed \u2014 it joins the roster immediately.",battery_fleet_track_self:"Track self-charging batteries",battery_fleet_track_self_hint:"Phones, vacuums and other devices that recharge themselves appear as rechargeables \u2014 a low one asks for a charge, never for new cells.",battery_fleet_status_low:"Low",battery_fleet_status_soon:"Soon",battery_fleet_status_ok:"Healthy",battery_fleet_predicted_on:"Expected around {date}",battery_fleet_predicted_trend:"Predicted from this battery's discharge trend: around {date} ({confidence})",battery_fleet_rechargeable:"Rechargeable: charge instead of replacing \u2014 never on the shopping list",battery_fleet_sort_name:"Sort by name",battery_fleet_sort_urgency:"Sort by urgency",battery_fleet_mark_recharged:"Mark as recharged",battery_fleet_sparkline_hint:"Battery level over the last 30 days \u2014 dotted: projected until the low threshold",battery_fleet_filter_type:"Show only this battery type",battery_fleet_record_replacement:"The level jumped around {date} \u2014 record this replacement in Battery Notes",battery_fleet_total:"{n} batteries tracked",battery_fleet_setup_button:"Battery fleet",battery_fleet_setup_done:"Battery fleet set up \u2014 one task tracks all your batteries.",update_banner:"A newer version of Maintenance Supporter is on the server \u2014 reload to update the panel.",update_reload:"Reload",row_actions_banner:"Complete and Skip in task rows are now buttons. Prefer the old icons?",row_actions_keep:"Keep buttons",row_actions_back:"Back to icons",battery_fleet_forecast_overdue:"Predicted date passed \u2014 the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",cost_from_parts:"Use \u2248 {amount} from parts",dismiss:"Dismiss",gs_label:"Getting started \u2014 these hints retire as your setup grows",gs_setups_chip:"Suggested setups found {n} devices with pre-wired triggers",gs_adopt_chip:"{n} problem sensors can become maintenance tasks",gs_fleet_chip:"One click sets up the battery fleet",cal_editor_window:"Default window",cal_editor_window_week:"Week (7 days)",cal_editor_window_fortnight:"Fortnight (14 days)",cal_editor_window_month:"Month (30 days, default)",cal_editor_window_year:"Year (365 days, empty days collapsed)",cal_editor_show_chips:"Show window chips inside the card",cal_editor_chips_hint:"Hide the chips when the card is embedded in a strategy view that already serves as the window selector.",cal_editor_show_user_filter:"Show user filter dropdown",cal_editor_default_user:"Default user filter",cal_editor_my_tasks:"My tasks (current user)",cal_editor_show_object_filter:"Show object filter dropdown",cal_editor_object_hint:'Pre-select one object via YAML: object_filter: "" \u2014 or a list of names to restrict the card to several objects.',object_history_section:"History (all tasks)",object_history_all_tasks:"All tasks",object_history_empty:"No entries in this range.",object_history_cap_note:"History keeps up to 500 entries per task \u2014 very old entries may be missing.",service_record_title:"Service record",service_record_print:"Service record (PDF)",date:"Date",service_record_entries:"entries",completed_by:"Completed by",date_from:"From",date_to:"To",phases_section:"Cycle phases (optional)",phases_hint:"Different work on one shared schedule \u2014 each completion moves to the next step (e.g. small service, small service, big service).",phase_add:"Add phase",phase_name:"Phase name",phase_sequence_label:"Cycle order",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 \u2014 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 \u201CRequire on completion\u201D 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 \u2014 saving here will be refused."}});var pi,ui=w(()=>{"use strict";pi="2.70.0"});var Ie,hi=w(()=>{"use strict";Ie={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"}});function qs(r){ce.en=Object.assign({},r,ce.en??{})}function qe(r){let s=(r||_t).toLowerCase();return s.startsWith("pt")&&s.endsWith("br")?"pt-br":s.substring(0,2)}function a(r,s){let e=qe(s);return ce[e]?.[r]??ce.en[r]??r}function mi(r,s){s.has("hass")&&Hs(r.hass?.locale,r.hass?.config?.country);let e=r.hass?.language;e&&!Re(e)&&Ne(e).then(()=>r.requestUpdate())}function D(r){return r?.language||"en"}function Re(r){let s=qe(r);return s===_t||s in ce}function Ne(r){let s=qe(r);return s===_t||s in ce||!Rs.has(s)?Promise.resolve():(s in Pe||(Pe[s]=fetch(`${Ns}/${s}.json?v=${pi}`).then(e=>e.ok?e.json():null).then(e=>{e?ce[s]=e:delete Pe[s]}).catch(()=>{delete Pe[s]})),Pe[s])}function fi(r){let s=qe(r);return{de:"de-DE",en:"en-US",nl:"nl-NL",fr:"fr-FR",it:"it-IT",es:"es-ES",pt:"pt-PT",ru:"ru-RU",uk:"uk-UA",zh:"zh-CN",da:"da-DK",fi:"fi-FI",nb:"nb-NO",ja:"ja-JP",hi:"hi-IN",pl:"pl-PL",cs:"cs-CZ",sv:"sv-SE","pt-br":"pt-BR",hu:"hu-HU",ko:"ko-KR",tr:"tr-TR"}[s]??"en-US"}function Hs(r,s){r&&(me.date=r.date_format,me.time=r.time_format,s!==void 0&&(me.country=s||void 0))}function Ke(r){let s=fi(r),e=me.country;if(e&&/^[A-Za-z]{2}$/.test(e)){let t=`${qe(r).split("-")[0]}-${e.toUpperCase()}`;try{return new Intl.DateTimeFormat(t),t}catch{}}return s}function vi(r,s){let e=String(r.getDate()).padStart(2,"0"),t=String(r.getMonth()+1).padStart(2,"0"),i=String(r.getFullYear());switch(me.date){case"DMY":return`${e}/${t}/${i}`;case"MDY":return`${t}/${e}/${i}`;case"YMD":return`${i}-${t}-${e}`;case"system":return r.toLocaleDateString(void 0,{day:"2-digit",month:"2-digit",year:"numeric"});default:return r.toLocaleDateString(Ke(s),{day:"2-digit",month:"2-digit",year:"numeric"})}}function Ds(r,s){switch(me.time){case"12":return r.toLocaleTimeString(Ke(s),{hour:"2-digit",minute:"2-digit",hour12:!0});case"24":return r.toLocaleTimeString(Ke(s),{hour:"2-digit",minute:"2-digit",hour12:!1});case"system":return r.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"});default:return r.toLocaleTimeString(Ke(s),{hour:"2-digit",minute:"2-digit"})}}function Q(r,s){if(!r)return"\u2014";try{let e=r.includes("T")?r:r+"T00:00:00";return vi(new Date(e),s)}catch{return r}}function yi(r,s){if(!r)return"\u2014";try{let e=new Date(r);return vi(e,s)+" "+Ds(e,s)}catch{return r}}function bi(r,s){if(r==null)return"\u2014";let e=s||"en";return r<0?`${Math.abs(r)} ${a("d_overdue",e)}`:r===0?a("today",e):`${r} ${a(r===1?"day":"days",e)}`}function Ge(r,s,e){return r==null?"\u2014":`${r} ${a("unit_"+(s||"days"),e)}`}function Le(r,s,e="long"){return new Date(Date.UTC(2024,0,1+r)).toLocaleDateString(fi(s),{weekday:e,timeZone:"UTC"})}function xi(r,s){let e=r.schedule,t=e?.offset?` ${e.offset>0?"+":"\u2212"}${Math.abs(e.offset)}d`:"";switch(e?.kind){case"weekdays":return((e.weekdays||[]).map(i=>Le(i,s,"short")).join(" & ")||"\u2014")+t;case"nth_weekday":return e.weekday==null||e.nth==null?"\u2014":`${e.nth===-1?a("ord_last",s):a("ord_"+e.nth,s)} ${Le(e.weekday,s,"long")}${t}`;case"day_of_month":return e.day==null?"\u2014":(e.day===-1?a(e.business?"last_business_day_month":"last_day_month",s):`${a("day_word",s)} ${e.day}`)+t;case"one_time":return r.due_date?Q(r.due_date,s):a("one_time",s);case"manual":return a("manual",s);case"interval":return Ge(e.every,e.unit,s)}return r.schedule_type==="one_time"?r.due_date?Q(r.due_date,s):a("one_time",s):r.schedule_type==="manual"?a("manual",s):r.schedule_type==="sensor_based"?a("sensor_based",s):r.interval_days!=null?Ge(r.interval_days,r.interval_unit,s):"\u2014"}function wi(r,s){r.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:s},bubbles:!0,composed:!0}))}var _i,_t,gi,ce,Rs,Ns,Pe,js,me,$i,Ye,N=w(()=>{"use strict";P();ci();ui();hi();_i="\u20AC",_t="en",gi=(()=>{let r=window;return r.__msLocales||(r.__msLocales={store:{},inflight:{}}),r.__msLocales})(),ce=gi.store;qs(di);Rs=new Set(["de","nl","fr","it","es","pt","pt-br","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi","hu","ko","tr"]),Ns="/maintenance_supporter_locales",Pe=gi.inflight;js=window,me=js.__msDateTimePrefs??={};$i=S` .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); } -`,Ge=S` +`,Ye=S` :host { --maint-ok-color: var(--success-color, #4caf50); --maint-due-soon-color: var(--warning-color, #ff9800); @@ -1149,7 +1149,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" .stat-item .stat-label { font-size: 11px; white-space: normal; text-align: center; line-height: 1.2; } .stat-value { font-size: 20px; } } -`});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` +`});function Os(r,s){let e=Ms[r];if(!e)return r;let t=a(e,s);return t&&t!==e?t:r}function zs(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 j(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=zs(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";N();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` `)} @@ -2225,7 +2225,7 @@ Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:" a("environmental_entity_optional",e)} .computeHelper=${()=>a("environmental_entity_helper",e)} @@ -2686,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,"_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="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue="";this._restockQty="";this._completedAt="";this._usedParts={};this.checklistPrefill={}}open(e={}){this._open||(this._open=!0,this.viaTagScan=!!e.viaTagScan,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((t,i)=>[String(i),!!this.checklistPrefill[t]]).filter(([,t])=>t)),this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._completedAt="",this._usedParts=Object.fromEntries(this.consumesParts.map(t=>[V(t),{...t}])))}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,i=t.files?.[0];if(t.value="",!!i){this._photoUploading=!0,this._error="";try{let n=new FormData;n.append("entry_id",this.entryId),n.append("tags","photo"),n.append("file",i,i.name);let o=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:n});if(!o.ok){this._error=o.status===413?a("doc_too_large",this.lang):a("doc_upload_failed",this.lang);return}let p=await o.json();p.id&&(this._photoDocId=p.id,this._photoPreview=URL.createObjectURL(i))}catch{this._error=a("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this.viaTagScan&&(e.via_tag_scan=!0),this._completedAt){if(new Date(this._completedAt).getTime()>Date.now()){this._error=a("completed_at_future_error",this.lang),this._loading=!1;return}e.completed_at=this._completedAt.length===16?`${this._completedAt}:00`:this._completedAt}if(this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=N(e,this.lang,a("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?l``:_}_partsCostSuggestion(){if(this.restockDefault!==null){let i=parseFloat(this._restockQty);return this.restockUnitCost==null||!Number.isFinite(i)||i<=0?null:Math.round(this.restockUnitCost*i*100)/100}if(!this.parts.length)return null;let e=0,t=!1;for(let i of Object.values(this._usedParts)){let n=this.parts.find(o=>V({part_id:o.id,entry_id:o.entry_id})===V(i));n?.cost!=null&&(e+=n.cost*(i.quantity||1),t=!0)}return t?Math.round(e*100)/100:null}_renderCostSuggestion(e){if(this._cost.trim()!=="")return _;let t=this._partsCostSuggestion();if(t==null||t<=0)return _;let i=`${t.toFixed(2)}${this.currencySymbol?` ${this.currencySymbol}`:""}`;return l` - ${c.cost!=null?o`${c.cost.toFixed(2)} ${this.currencySymbol}`:d} - ${c.duration!=null?o`${c.duration} min`:d} + ${c.cost!=null?r`${c.cost.toFixed(2)} ${this.currencySymbol}`:d} + ${c.duration!=null?r`${c.duration} min`:d} - ${c.notes?o`${c.notes}`:d} + ${c.notes?r`${c.notes}`:d} `)} - ${i.length>p.length?o`{this._expanded=!0}}> + ${i.length>p.length?r`{this._expanded=!0}}> ${s("show_all",t)} (${i.length}) `:d}
${a} ${s("service_record_entries",t)} · ${s("report_total_cost",t)}: - ${l.toFixed(2)} ${this.currencySymbol} + ${n.toFixed(2)} ${this.currencySymbol}
- ${this._capped?o`

${s("object_history_cap_note",t)}

`:d} + ${this._capped?r`

${s("object_history_cap_note",t)}

`:d} `} - `}};G.styles=D` + `}};K.styles=R` .section { margin-top: 28px; } h3 { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; } .count { @@ -2580,7 +2660,7 @@ ${p.capped?`

${L(t.capNote)}

`:""} @media (max-width: 640px) { .notes { padding-left: 0; } } - `,u([$({attribute:!1})],G.prototype,"hass",2),u([$()],G.prototype,"entryId",2),u([$({attribute:!1})],G.prototype,"object",2),u([$({attribute:!1})],G.prototype,"tasks",2),u([$()],G.prototype,"currencySymbol",2),u([$({attribute:!1})],G.prototype,"userName",2),u([m()],G.prototype,"_full",2),u([m()],G.prototype,"_loading",2),u([m()],G.prototype,"_filterTask",2),u([m()],G.prototype,"_from",2),u([m()],G.prototype,"_to",2),u([m()],G.prototype,"_expanded",2);customElements.get("maintenance-object-history-section")||customElements.define("maintenance-object-history-section",G);var tt=class tt extends R{constructor(){super(...arguments);this.flat=!1;this._ov=null;this._loading=!1;this._marking=!1;this._error="";this._history=null;this._rosterSort=tt._storedSort();this._typeFilter=null;this._recorded=[];this._historyRequested=!1;this._localeReady=!1;this._markAll=async()=>{await this._mark(void 0)};this._repair=async()=>{if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup",language:this._lang}),await this._load()}catch(t){this._error=S(t,this._lang)}finally{this._marking=!1}}};this._loadHistory=async t=>{if(!(!t.target.open||this._historyRequested)){this._historyRequested=!0;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview_history"});this._history=e.series}catch{this._history=null}}}}get _lang(){return P(this.hass)}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,Y(this._lang).then(()=>this.requestUpdate()),this._ov===null&&!this._loading&&this._load())}async _load(){this._loading=!0,this._error="";try{this._ov=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview"})}catch(t){this._error=S(t,this._lang)}finally{this._loading=!1}}async _mark(t){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/mark_replaced",...t?{entity_ids:t}:{}}),await this._load()}catch(e){this._error=S(e,this._lang)}finally{this._marking=!1}}}async _setExcluded(t,e){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/set_excluded",entity_id:t,excluded:e}),await this._load()}catch(i){this._error=S(i,this._lang)}finally{this._marking=!1}}}async _addBattery(t){let e=t.detail?.value;if(!(!e||this._marking)){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/set_included",entity_id:e,included:!0}),await this._load()}catch(i){this._error=S(i,this._lang)}finally{this._marking=!1}}}async _setTrackSelf(t){let e=t.target.checked;if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/set_track_self_charging",enabled:e}),await this._load()}catch(i){this._error=S(i,this._lang)}finally{this._marking=!1}}}_sparkline(t){let e=this._history?.[t.entity_id];if(!e||e.points.length<2)return d;let i=110,a=24,l=2,p=e.points[0][0],c=e.points[e.points.length-1][0],h=Date.now()/1e3,g=t.status!=="low"&&t.predicted_source==="trend"&&t.days_until!=null?h+t.days_until*86400:null,v=Math.max(c,g??c),_=E=>v===p?l:l+(E-p)/(v-p)*(i-2*l),y=E=>l+(1-Math.min(100,Math.max(0,E))/100)*(a-2*l),k=e.points.map(([E,w])=>`${_(E).toFixed(1)},${y(w).toFixed(1)}`).join(" "),j=e.points[e.points.length-1][1],M=y(e.threshold).toFixed(1);return o`{await this._mark(void 0)};this._repair=async()=>{if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup",language:this._lang}),await this._load()}catch(t){this._error=S(t,this._lang)}finally{this._marking=!1}}};this._loadHistory=async t=>{if(!(!t.target.open||this._historyRequested)){this._historyRequested=!0;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview_history"});this._history=e.series}catch{this._history=null}}}}get _lang(){return L(this.hass)}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,Y(this._lang).then(()=>this.requestUpdate()),this._ov===null&&!this._loading&&this._load())}async _load(){this._loading=!0,this._error="";try{this._ov=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview"})}catch(t){this._error=S(t,this._lang)}finally{this._loading=!1}}async _mark(t){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/mark_replaced",...t?{entity_ids:t}:{}}),await this._load()}catch(e){this._error=S(e,this._lang)}finally{this._marking=!1}}}async _setExcluded(t,e){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/set_excluded",entity_id:t,excluded:e}),await this._load()}catch(i){this._error=S(i,this._lang)}finally{this._marking=!1}}}async _addBattery(t){let e=t.detail?.value;if(!(!e||this._marking)){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/set_included",entity_id:e,included:!0}),await this._load()}catch(i){this._error=S(i,this._lang)}finally{this._marking=!1}}}async _setTrackSelf(t){let e=t.target.checked;if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/set_track_self_charging",enabled:e}),await this._load()}catch(i){this._error=S(i,this._lang)}finally{this._marking=!1}}}_sparkline(t){let e=this._history?.[t.entity_id];if(!e||e.points.length<2)return d;let i=110,a=24,n=2,p=e.points[0][0],c=e.points[e.points.length-1][0],h=Date.now()/1e3,g=t.status!=="low"&&t.predicted_source==="trend"&&t.days_until!=null?h+t.days_until*86400:null,v=Math.max(c,g??c),_=E=>v===p?n:n+(E-p)/(v-p)*(i-2*n),y=E=>n+(1-Math.min(100,Math.max(0,E))/100)*(a-2*n),k=e.points.map(([E,$])=>`${_(E).toFixed(1)},${y($).toFixed(1)}`).join(" "),j=e.points[e.points.length-1][1],M=y(e.threshold).toFixed(1);return r`${L(t.capNote)}

`:""} ${s("battery_fleet_sparkline_hint",this._lang)} - ${g!==null?o``:d} -
`}static _storedSort(){return J(A.batteryRosterSort)==="name"?"name":"urgency"}_setSort(t){this._rosterSort=t,q(A.batteryRosterSort,t)}_sortedRoster(t){let e=this._typeFilter===null?t:t.filter(a=>a.battery_type===this._typeFilter);if(this._rosterSort==="name")return e;let i=a=>a.status==="low"?-1e3+(a.level??101)/101:a.days_until??1/0;return[...e].sort((a,l)=>i(a)-i(l)||a.device_name.localeCompare(l.device_name))}_predictedDate(t){return this._fmtDate(Date.now()+t*864e5)}_fmtDate(t){return new Intl.DateTimeFormat(this._lang,{day:"numeric",month:"numeric",year:"numeric"}).format(new Date(t))}_shoppingLine(t){return Object.entries(t).map(([e,i])=>o`
`}render(){let t=this._lang;if(this._loading&&this._ov===null)return r`
…
`;let e=this._ov;if(!e)return this._error?r`
${this._error}
`:d;let i=e.low.length;return r`
${s("battery_fleet_title",t)} ${i}
- ${this._error?o`
${this._error}
`:d} + ${this._error?r`
${this._error}
`:d} - ${e.configured&&e.task_ok===!1?o` + ${e.configured&&e.task_ok===!1?r`
${s("battery_fleet_trigger_lost",t)} @@ -2622,22 +2702,22 @@ ${p.capped?`

${L(t.capNote)}

`:""}
`:d} - ${i===0?o`
${s("battery_fleet_none_low",t)}
`:o` + ${i===0?r`
${s("battery_fleet_none_low",t)}
`:r`
${s("battery_fleet_buy_now",t)} ${this._shoppingLine(e.needs_now)}
- ${e.low.map(a=>o` + ${e.low.map(a=>r`
${a.device_name} - ${a.available===!1?o`${s("battery_fleet_offline",t)}`:d} + ${a.available===!1?r`${s("battery_fleet_offline",t)}`:d} ${a.quantity}× ${a.battery_type} - ${a.rechargeable?o``:d} ${this._levelBar(a)} - ${a.level!=null?o`${a.level}%`:d} + ${a.level!=null?r`${a.level}%`:d}
`} - ${e.soon.length?o` + ${e.soon.length?r`
${s("battery_fleet_soon",t)} ${this._shoppingLine(e.needs_soon)}
${s("battery_fleet_soon_hint",t)}
`:d} - ${e.all?.length?o` + ${e.all?.length?r`
${s("battery_fleet_all",t)} (${e.all.length})
@@ -2689,29 +2769,29 @@ ${p.capped?`

${L(t.capNote)}

`:""}
- ${this._sortedRoster(e.all).map(a=>o` + ${this._sortedRoster(e.all).map(a=>r`
${a.device_name} ${s("battery_fleet_status_"+a.status,t)} ${a.quantity}× ${a.battery_type} - ${a.rechargeable?o``:d} ${this._sparkline(a)} ${this._levelBar(a)} - ${a.level!=null?o`${a.level}%`:d} - ${(()=>{let l=this._history?.[a.entity_id]?.jump;return!l||this._recorded.includes(a.entity_id)?d:o``})()} - ${a.days_until!=null?o`${a.forecast_overdue?o``:d}~${this._predictedDate(a.days_until)}${a.forecast_overdue?r``:d}~${this._predictedDate(a.days_until)}`:d}
`:d} - ${e.excluded?.length?o` + ${e.excluded?.length?r`
${s("battery_fleet_excluded",t)} - ${e.excluded.map(a=>o` + ${e.excluded.map(a=>r` ${a.device_name}
- `}};tt.styles=D` + `}};tt.styles=R` .bf-card { background: var(--card-background-color, #fff); border: 1px solid var(--divider-color); @@ -3155,7 +3235,7 @@ ${p.capped?`

${L(t.capNote)}

`:""} accent-color: var(--primary-color); margin: 0; } - `,u([$({attribute:!1})],tt.prototype,"hass",2),u([$({type:Boolean})],tt.prototype,"flat",2),u([m()],tt.prototype,"_ov",2),u([m()],tt.prototype,"_loading",2),u([m()],tt.prototype,"_marking",2),u([m()],tt.prototype,"_error",2),u([m()],tt.prototype,"_history",2),u([m()],tt.prototype,"_rosterSort",2),u([m()],tt.prototype,"_typeFilter",2),u([m()],tt.prototype,"_recorded",2);var se=tt;customElements.get("maintenance-battery-fleet-section")||customElements.define("maintenance-battery-fleet-section",se);var He=D` + `,u([w({attribute:!1})],tt.prototype,"hass",2),u([w({type:Boolean})],tt.prototype,"flat",2),u([m()],tt.prototype,"_ov",2),u([m()],tt.prototype,"_loading",2),u([m()],tt.prototype,"_marking",2),u([m()],tt.prototype,"_error",2),u([m()],tt.prototype,"_history",2),u([m()],tt.prototype,"_rosterSort",2),u([m()],tt.prototype,"_typeFilter",2),u([m()],tt.prototype,"_recorded",2);var se=tt;customElements.get("maintenance-battery-fleet-section")||customElements.define("maintenance-battery-fleet-section",se);var He=R` .cal-controls { display: flex; gap: 12px; @@ -3352,12 +3432,12 @@ ${p.capped?`

${L(t.capNote)}

`:""} .cal-pill-day { font-size: 17px; } .cal-user-filter { margin-left: 0; width: 100%; } } -`;function Ne(r){let n=window;n.customCards=n.customCards||[],n.customCards.some(t=>t.type===r.type)||n.customCards.push(r)}var st=class extends R{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(t){if(this._config={...t},t.past_days&&[30,90].includes(t.past_days)?this._pastDays=t.past_days:t.window_days&&[7,14,30,365].includes(t.window_days)&&(this._windowDays=t.window_days,this._pastDays=0),typeof t.user_filter=="string"&&(this._userFilter=t.user_filter),typeof t.object_filter=="string")this._objectFilter=t.object_filter,this._configuredObjects=[];else if(Array.isArray(t.object_filter)){let e=t.object_filter.filter(i=>typeof i=="string"&&i!=="");this._objectFilter=e.length===1?e[0]:"",this._configuredObjects=e.length>1?e:[]}}getCardSize(){return 6}get _lang(){return P(this.hass)}disconnectedCallback(){if(super.disconnectedCallback(),this._unsub){try{this._unsub()}catch{}this._unsub=null}this._dataLoaded=!1,this._lastConnection=null}updated(t){if(super.updated(t),Pt(this,t),t.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[t,e]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/statistics"})]);this._objects=t.objects,this._stats=e}catch{}}async _subscribe(){try{let t=await this.hass.connection.subscribeMessage(e=>{let i=e;this._objects=i.objects},{type:"maintenance_supporter/subscribe"});if(!this.isConnected){t();return}this._unsub=t}catch{}}_onEventClick(t){if(t.history_timestamp){this._openHistoryEntry(t);return}De(t.entry_id,t.task_id)||this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:open-task",entry_id:t.entry_id,task_id:t.task_id},bubbles:!0,composed:!0}))}async _openHistoryEntry(t){try{let i=(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:t.entry_id})).tasks?.find(l=>l.id===t.task_id)?.history?.find(l=>l.timestamp===t.history_timestamp);if(!i||Ce({entry_id:t.entry_id,task_id:t.task_id,original_timestamp:t.history_timestamp,type:i.type||"completed",timestamp:i.timestamp||t.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:t.entry_id,task_id:t.task_id,original_timestamp:t.history_timestamp},bubbles:!0,composed:!0}))}render(){if(!this.hass)return d;let t=this._lang,e=this._config.show_window_chips!==!1,i=this._config.show_user_filter!==!1,a=this._config.title,l=null;this._userFilter&&(l=this._userFilter==="current_user"?this.hass?.user?.id??null:this._userFilter);let p=f=>{let Q=f.toLowerCase();return this._objects.find(it=>it.entry_id===f||it.object.name.toLowerCase()===Q)?.entry_id??null},c=new Set(this._configuredObjects.map(p).filter(f=>f!==null)),h=c.size?this._objects.filter(f=>c.has(f.entry_id)):this._objects,g=this._config.show_object_filter!==!1&&h.length>1,v=this._objectFilter?p(this._objectFilter):null,_=v&&h.some(f=>f.entry_id===v)?h.filter(f=>f.entry_id===v):h,y=new Date;y.setHours(0,0,0,0);let k=this._pastDays>0,j=k?ke(_,y,this._pastDays,l):we(_,y,this._windowDays,l),M=$e(y),E=this._windowDays===365||k,w=E?j.filter(f=>f.events.length>0):j,z=f=>{let Q=`cal-status-${f.status}`,et=f.projected?"cal-event-projected":"",it=f.status==="overdue"&&f.days_until_due!=null?` (${_t(f.days_until_due,t)})`:"",nt=f.projected&&f.interval_days?o`${f.interval_unit&&f.interval_unit!=="days"?`${f.interval_days} ${s("unit_"+f.interval_unit,t)}`:s("cal_every_n_days",t).replace("{n}",String(f.interval_days))}`:d,U=f.schedule_type==="sensor_based",dt=U?o``:o`t.type===o.type)||l.customCards.push(o)}var st=class extends O{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(t){if(this._config={...t},t.past_days&&[30,90].includes(t.past_days)?this._pastDays=t.past_days:t.window_days&&[7,14,30,365].includes(t.window_days)&&(this._windowDays=t.window_days,this._pastDays=0),typeof t.user_filter=="string"&&(this._userFilter=t.user_filter),typeof t.object_filter=="string")this._objectFilter=t.object_filter,this._configuredObjects=[];else if(Array.isArray(t.object_filter)){let e=t.object_filter.filter(i=>typeof i=="string"&&i!=="");this._objectFilter=e.length===1?e[0]:"",this._configuredObjects=e.length>1?e:[]}}getCardSize(){return 6}get _lang(){return L(this.hass)}disconnectedCallback(){if(super.disconnectedCallback(),this._unsub){try{this._unsub()}catch{}this._unsub=null}this._dataLoaded=!1,this._lastConnection=null}updated(t){if(super.updated(t),Lt(this,t),t.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[t,e]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/statistics"})]);this._objects=t.objects,this._stats=e}catch{}}async _subscribe(){try{let t=await this.hass.connection.subscribeMessage(e=>{let i=e;this._objects=i.objects},{type:"maintenance_supporter/subscribe"});if(!this.isConnected){t();return}this._unsub=t}catch{}}_onEventClick(t){if(t.history_timestamp){this._openHistoryEntry(t);return}Re(t.entry_id,t.task_id)||this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:open-task",entry_id:t.entry_id,task_id:t.task_id},bubbles:!0,composed:!0}))}async _openHistoryEntry(t){try{let i=(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:t.entry_id})).tasks?.find(n=>n.id===t.task_id)?.history?.find(n=>n.timestamp===t.history_timestamp);if(!i||Ce({entry_id:t.entry_id,task_id:t.task_id,original_timestamp:t.history_timestamp,type:i.type||"completed",timestamp:i.timestamp||t.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:t.entry_id,task_id:t.task_id,original_timestamp:t.history_timestamp},bubbles:!0,composed:!0}))}render(){if(!this.hass)return d;let t=this._lang,e=this._config.show_window_chips!==!1,i=this._config.show_user_filter!==!1,a=this._config.title,n=null;this._userFilter&&(n=this._userFilter==="current_user"?this.hass?.user?.id??null:this._userFilter);let p=f=>{let Q=f.toLowerCase();return this._objects.find(it=>it.entry_id===f||it.object.name.toLowerCase()===Q)?.entry_id??null},c=new Set(this._configuredObjects.map(p).filter(f=>f!==null)),h=c.size?this._objects.filter(f=>c.has(f.entry_id)):this._objects,g=this._config.show_object_filter!==!1&&h.length>1,v=this._objectFilter?p(this._objectFilter):null,_=v&&h.some(f=>f.entry_id===v)?h.filter(f=>f.entry_id===v):h,y=new Date;y.setHours(0,0,0,0);let k=this._pastDays>0,j=k?ke(_,y,this._pastDays,n):$e(_,y,this._windowDays,n),M=we(y),E=this._windowDays===365||k,$=E?j.filter(f=>f.events.length>0):j,z=f=>{let Q=`cal-status-${f.status}`,et=f.projected?"cal-event-projected":"",it=f.status==="overdue"&&f.days_until_due!=null?` (${_t(f.days_until_due,t)})`:"",nt=f.projected&&f.interval_days?r`${f.interval_unit&&f.interval_unit!=="days"?`${f.interval_days} ${s("unit_"+f.interval_unit,t)}`:s("cal_every_n_days",t).replace("{n}",String(f.interval_days))}`:d,U=f.schedule_type==="sensor_based",dt=U?r``:r``,yt=U&&f.prediction_confidence&&f.status!=="triggered"&&!f.projected?o` + icon="${f.adaptive_enabled?"mdi:clock-time-four-outline":"mdi:clock-outline"}">`,yt=U&&f.prediction_confidence&&f.status!=="triggered"&&!f.projected?r` ${s("cal_predicted",t)} · ${s(`cal_confidence_${f.prediction_confidence}`,t)} - `:d,xt=this._stats?.budget?.currency_symbol||Lt,B=f.history_type?s(f.history_type,t):s(f.status,t);return o` + `:d,xt=this._stats?.budget?.currency_symbol||Pt,B=f.history_type?s(f.history_type,t):s(f.status,t);return r`
this._onEventClick(f)}> ${dt} @@ -3367,9 +3447,9 @@ ${p.capped?`

${L(t.capNote)}

`:""} ${yt} ${nt}
- ${f.avg_cost!=null&&f.avg_cost>0?o`${f.avg_cost.toFixed(0)} ${xt}`:d} + ${f.avg_cost!=null&&f.avg_cost>0?r`${f.avg_cost.toFixed(0)} ${xt}`:d}
- `},T=f=>{let[Q,et,it]=f.date.split("-").map(Number),nt=new Date(Q,et-1,it),U=f.date===M,dt=nt.toLocaleDateString(t,{weekday:"short"}),yt=nt.toLocaleDateString(t,{month:"long"});return o` + `},T=f=>{let[Q,et,it]=f.date.split("-").map(Number),nt=new Date(Q,et-1,it),U=f.date===M,dt=nt.toLocaleDateString(t,{weekday:"short"}),yt=nt.toLocaleDateString(t,{month:"long"});return r`
${dt} @@ -3378,18 +3458,18 @@ ${p.capped?`

${L(t.capNote)}

`:""}
${yt} - ${U?o`${s("today",t)}`:d} + ${U?r`${s("today",t)}`:d}
- ${f.events.length===0?o`
${s("cal_no_events",t)}
`:f.events.map(z)} + ${f.events.length===0?r`
${s("cal_no_events",t)}
`:f.events.map(z)}
- `};return o` + `};return r` - ${e||i?o` + ${e||i?r`
- ${e?o` + ${e?r`
- ${[30,90].map(f=>o` + ${[30,90].map(f=>r`
- ${[7,14,30,365].map(f=>o` + ${[7,14,30,365].map(f=>r`
`:d} - ${i?o` + ${i?r` `:d} - ${g?o` + ${g?r` `:d}
`:d}
- ${w.length===0&&E?o`
${s("cal_no_events",t)}
`:w.map(T)} + ${$.length===0&&E?r`
${s("cal_no_events",t)}
`:$.map(T)}
- `}};st.styles=[Ft,He,D` + `}};st.styles=[Ft,He,R` :host { display: block; } ha-card { padding: 0; overflow: hidden; } - `],u([$({attribute:!1})],st.prototype,"hass",2),u([m()],st.prototype,"_config",2),u([m()],st.prototype,"_objects",2),u([m()],st.prototype,"_stats",2),u([m()],st.prototype,"_windowDays",2),u([m()],st.prototype,"_pastDays",2),u([m()],st.prototype,"_userFilter",2),u([m()],st.prototype,"_objectFilter",2),u([m()],st.prototype,"_unsub",2);var hi=[{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"}],Tt=class extends R{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"}}get _lang(){return P(this.hass)}setConfig(t){this._config={...t}}updated(){let t=this._lang;t&&!de(t)&&Y(t).then(()=>this.requestUpdate())}_valueChanged(t,e){let i={...this._config,[t]:e};t==="show_window_chips"&&e===!0&&delete i.show_window_chips,t==="show_user_filter"&&e===!0&&delete i.show_user_filter,t==="show_object_filter"&&e===!0&&delete i.show_object_filter,t==="title"&&(!e||typeof e=="string"&&e.trim()==="")&&delete i.title,t==="user_filter"&&e===""&&delete i.user_filter,this._config=i,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:i},bubbles:!0,composed:!0}))}render(){let t=this._lang,e=this._config.window_days??30,i=this._config.show_window_chips!==!1,a=this._config.show_user_filter!==!1,l=this._config.user_filter??"",p=this._config.title??"";return o` + `],u([w({attribute:!1})],st.prototype,"hass",2),u([m()],st.prototype,"_config",2),u([m()],st.prototype,"_objects",2),u([m()],st.prototype,"_stats",2),u([m()],st.prototype,"_windowDays",2),u([m()],st.prototype,"_pastDays",2),u([m()],st.prototype,"_userFilter",2),u([m()],st.prototype,"_objectFilter",2),u([m()],st.prototype,"_unsub",2);var hi=[{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"}],Tt=class extends O{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"}}get _lang(){return L(this.hass)}setConfig(t){this._config={...t}}updated(){let t=this._lang;t&&!de(t)&&Y(t).then(()=>this.requestUpdate())}_valueChanged(t,e){let i={...this._config,[t]:e};t==="show_window_chips"&&e===!0&&delete i.show_window_chips,t==="show_user_filter"&&e===!0&&delete i.show_user_filter,t==="show_object_filter"&&e===!0&&delete i.show_object_filter,t==="title"&&(!e||typeof e=="string"&&e.trim()==="")&&delete i.title,t==="user_filter"&&e===""&&delete i.user_filter,this._config=i,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:i},bubbles:!0,composed:!0}))}render(){let t=this._lang,e=this._config.window_days??30,i=this._config.show_window_chips!==!1,a=this._config.show_user_filter!==!1,n=this._config.user_filter??"",p=this._config.title??"";return r`
@@ -3448,7 +3528,7 @@ ${p.capped?`

${L(t.capNote)}

`:""} id="window" @change=${c=>this._valueChanged("window_days",Number(c.target.value))} > - ${hi.map(c=>o``)} + ${hi.map(c=>r``)}
@@ -3476,8 +3556,8 @@ ${p.capped?`

${L(t.capNote)}

`:""} id="userv" @change=${c=>this._valueChanged("user_filter",c.target.value)} > - - + @@ -3493,7 +3573,7 @@ ${p.capped?`

${L(t.capNote)}

`:""}
${s("cal_editor_object_hint",t)}
- `}};Tt.styles=D` + `}};Tt.styles=R` :host { display: block; padding: 8px 0; } .editor { display: flex; flex-direction: column; gap: 12px; } .row { display: flex; flex-direction: column; gap: 4px; } @@ -3517,12 +3597,12 @@ ${p.capped?`

${L(t.capNote)}

`:""} font-size: 12px; color: var(--secondary-text-color, #666); } - `,u([$({attribute:!1})],Tt.prototype,"hass",2),u([m()],Tt.prototype,"_config",2);customElements.get("maintenance-supporter-calendar-card")||customElements.define("maintenance-supporter-calendar-card",st);customElements.get("maintenance-supporter-calendar-card-editor")||customElements.define("maintenance-supporter-calendar-card-editor",Tt);Ne({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});var at=class extends R{constructor(){super(...arguments);this._open=!1;this._title="";this._message="";this._confirmText="";this._danger=!1;this._inputLabel="";this._inputType="";this._inputValue="";this._resolve=null;this._promptResolve=null}confirm(t){return this._title=t.title,this._message=t.message,this._confirmText=t.confirmText||"OK",this._danger=t.danger||!1,this._inputLabel="",this._inputType="",this._inputValue="",this._open=!0,new Promise(e=>{this._resolve=e,this._promptResolve=null})}prompt(t){return this._title=t.title,this._message=t.message,this._confirmText=t.confirmText||"OK",this._danger=t.danger||!1,this._inputLabel=t.inputLabel||"",this._inputType=t.inputType||"text",this._inputValue=t.inputValue||"",this._open=!0,new Promise(e=>{this._promptResolve=e,this._resolve=null})}_cancel(){this._open=!1,this._promptResolve&&(this._promptResolve({confirmed:!1,value:""}),this._promptResolve=null),this._resolve?.(!1),this._resolve=null}_confirmAction(){this._open=!1,this._promptResolve&&(this._promptResolve({confirmed:!0,value:this._inputValue}),this._promptResolve=null),this._resolve?.(!0),this._resolve=null}render(){if(!this._open)return d;let t=P(this.hass);return o` + `,u([w({attribute:!1})],Tt.prototype,"hass",2),u([m()],Tt.prototype,"_config",2);customElements.get("maintenance-supporter-calendar-card")||customElements.define("maintenance-supporter-calendar-card",st);customElements.get("maintenance-supporter-calendar-card-editor")||customElements.define("maintenance-supporter-calendar-card-editor",Tt);Ne({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});var at=class extends O{constructor(){super(...arguments);this._open=!1;this._title="";this._message="";this._confirmText="";this._danger=!1;this._inputLabel="";this._inputType="";this._inputValue="";this._resolve=null;this._promptResolve=null}confirm(t){return this._title=t.title,this._message=t.message,this._confirmText=t.confirmText||"OK",this._danger=t.danger||!1,this._inputLabel="",this._inputType="",this._inputValue="",this._open=!0,new Promise(e=>{this._resolve=e,this._promptResolve=null})}prompt(t){return this._title=t.title,this._message=t.message,this._confirmText=t.confirmText||"OK",this._danger=t.danger||!1,this._inputLabel=t.inputLabel||"",this._inputType=t.inputType||"text",this._inputValue=t.inputValue||"",this._open=!0,new Promise(e=>{this._promptResolve=e,this._resolve=null})}_cancel(){this._open=!1,this._promptResolve&&(this._promptResolve({confirmed:!1,value:""}),this._promptResolve=null),this._resolve?.(!1),this._resolve=null}_confirmAction(){this._open=!1,this._promptResolve&&(this._promptResolve({confirmed:!0,value:this._inputValue}),this._promptResolve=null),this._resolve?.(!0),this._resolve=null}render(){if(!this._open)return d;let t=L(this.hass);return r`
${this._title}
${this._message} - ${this._inputLabel?o` + ${this._inputLabel?r` + + + + +
+ ${r("completion_photo_optional",e)}${this._req("photo")} + ${this._photoPreview?o` +
+ + +
`:o` + `} +
+ ${this.adaptiveEnabled?o` + + `:d} +
+
+ + ${r("cancel",e)} + + 0} + title=${this._missingRequired.length?this._missingRequired.map(t=>r("err_required",e).replace("{field}",r(k[t]??t,e))).join(" \xB7 "):""} + > + ${this._loading?r("completing",e):r("complete",e)} + +
+
+ `}};i.styles=[g,b` + .req-mark { + color: var(--error-color, #f44336); + margin-left: 2px; + font-weight: 600; + } + /* #104: one-click cost suggestion from parts — quiet link-style chip. */ + .cost-suggestion { + align-self: flex-start; + margin-top: 4px; + padding: 0; + border: none; + background: none; + color: var(--primary-color); + font-size: 12.5px; + cursor: pointer; + text-decoration: underline dotted; + text-underline-offset: 2px; + } + .dialog-title { + font-size: 18px; + 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; + font-size: 13px; + color: var(--secondary-text-color); + } + .content { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 300px; + } + .dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + } + .consumes-hint { + font-size: 13px; + color: var(--secondary-text-color); + border-left: 3px solid var(--primary-color); + padding: 4px 8px; + margin: 4px 0 8px; + } + /* #99: editable per-completion parts selection */ + .used-parts { margin: 4px 0 8px; display: flex; flex-direction: column; gap: 4px; } + .used-part-row { display: flex; align-items: center; gap: 8px; } + .used-part-check { + display: flex; align-items: center; gap: 6px; flex: 1; + font-size: 13px; cursor: pointer; + } + .used-part-check input { cursor: pointer; } + /* #111: whose stock this row draws on. Muted but never omitted — an + unlabelled foreign pool is indistinguishable from an own part. */ + .used-part-owner { color: var(--secondary-text-color); } + .used-part-qty { + width: 76px; padding: 4px 6px; border-radius: 4px; font: inherit; font-size: 13px; + border: 1px solid var(--divider-color); + background: var(--card-background-color); + color: var(--primary-text-color); + } + .error { + color: var(--error-color, #f44336); + font-size: 13px; + } + /* .field/.field-label/.field-input come from nativeFieldStyles */ + .photo-pick { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1px dashed var(--divider-color); + border-radius: 8px; + cursor: pointer; + font-size: 13px; + color: var(--secondary-text-color); + width: fit-content; + } + .photo-pick:hover { border-color: var(--primary-color); } + .photo-pick input[type="file"] { display: none; } + .photo-preview { + position: relative; + width: fit-content; + } + .photo-preview img { + max-width: 160px; + max-height: 160px; + border-radius: 8px; + display: block; + } + .photo-remove { + position: absolute; + top: -8px; + right: -8px; + width: 24px; + height: 24px; + border-radius: 50%; + border: none; + background: var(--error-color, #db4437); + color: #fff; + cursor: pointer; + font-size: 12px; + line-height: 1; + } + .checklist-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + margin-bottom: 4px; + } + .checklist-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + .checklist-item { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 4px 0; + font-size: 14px; + } + .checklist-item input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + } + .feedback-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color); + } + .feedback-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + .feedback-buttons { + display: flex; + gap: 8px; + } + .feedback-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + text-align: center; + transition: all 0.2s; + } + .feedback-btn:hover { + background: var(--secondary-background-color, #f5f5f5); + } + .feedback-btn.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + `],s([n({attribute:!1})],i.prototype,"hass",2),s([n()],i.prototype,"entryId",2),s([n()],i.prototype,"taskId",2),s([n()],i.prototype,"taskName",2),s([n()],i.prototype,"lang",2),s([n({type:Array})],i.prototype,"checklist",2),s([n({type:Boolean})],i.prototype,"adaptiveEnabled",2),s([n()],i.prototype,"taskType",2),s([n()],i.prototype,"readingUnit",2),s([n({attribute:!1})],i.prototype,"restockDefault",2),s([n({attribute:!1})],i.prototype,"restockUnitCost",2),s([n()],i.prototype,"currencySymbol",2),s([n({attribute:!1})],i.prototype,"parts",2),s([n({attribute:!1})],i.prototype,"consumesParts",2),s([n({type:Array})],i.prototype,"consumesInfo",2),s([n({type:Array})],i.prototype,"requiredFields",2),s([n()],i.prototype,"phaseLabel",2),s([n({type:Boolean})],i.prototype,"requireTagScan",2),s([n({type:Boolean})],i.prototype,"viaTagScan",2),s([l()],i.prototype,"_open",2),s([l()],i.prototype,"_notes",2),s([l()],i.prototype,"_cost",2),s([l()],i.prototype,"_duration",2),s([l()],i.prototype,"_loading",2),s([l()],i.prototype,"_error",2),s([l()],i.prototype,"_checklistState",2),s([l()],i.prototype,"_feedback",2),s([l()],i.prototype,"_photoDocId",2),s([l()],i.prototype,"_photoPreview",2),s([l()],i.prototype,"_photoUploading",2),s([l()],i.prototype,"_readingValue",2),s([l()],i.prototype,"_restockQty",2),s([l()],i.prototype,"_completedAt",2),s([l()],i.prototype,"_usedParts",2),s([n({attribute:!1})],i.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",i);export{i as a}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-SYT5D73A.js b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-SYT5D73A.js new file mode 100644 index 00000000..f6fc2ae7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-SYT5D73A.js @@ -0,0 +1,79 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{a as r,b as l,c as i,f as o,h as n,l as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";var e=class extends n{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(s){let a=s.target.value;this.value=a,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:a}}))}render(){return i` + + `}};e.styles=l` + :host { display: block; } + .field { + display: flex; + flex-direction: column; + gap: 4px; + } + .label { + font-size: 12px; + color: var(--secondary-text-color, #888); + font-weight: 500; + } + .req { color: var(--error-color, #f44336); margin-left: 2px; } + input { + 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; + } + input:focus { + 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; + } + `,r([t()],e.prototype,"label",2),r([t()],e.prototype,"value",2),r([t()],e.prototype,"placeholder",2),r([t()],e.prototype,"type",2),r([t({type:Boolean})],e.prototype,"required",2),r([t({type:Boolean})],e.prototype,"disabled",2),r([t()],e.prototype,"step",2),r([t()],e.prototype,"min",2),r([t()],e.prototype,"max",2),r([t()],e.prototype,"pattern",2),r([t()],e.prototype,"helper",2),r([t({type:Boolean})],e.prototype,"multiline",2),r([t({type:Number})],e.prototype,"rows",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",e); diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-UGIIDVTL.js b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-UGIIDVTL.js new file mode 100644 index 00000000..6bf76c5d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-UGIIDVTL.js @@ -0,0 +1,2 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{b as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3L2PFMVS.js";async function l(e,n,t=300){return(await e.connection.sendMessagePromise({type:"auth/sign_path",path:n,expires:t})).path}async function r(e,n,t=300){return l(e,`/api/maintenance_supporter/document/${n}`,t)}async function y(e,n,t=""){let a=window.open("about:blank","_blank");try{let o=await r(e,n);a&&(a.location.href=new URL(o+t,window.location.origin).href)}catch(o){throw a&&a.close(),o}}async function p(e,n,t){i(await r(e,n,30),t)}function d(e){let n=URL.createObjectURL(new Blob([e],{type:"text/html"}));window.open(n,"_blank"),setTimeout(()=>URL.revokeObjectURL(n),6e4)}var c=[{key:"name",labelKey:"name",required:!0},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],u=c.map(e=>e.key),s=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function g(e){if(!Array.isArray(e))return[...s];let n=new Set,t=[];for(let a of e)typeof a=="string"&&u.includes(a)&&!n.has(a)&&(n.add(a),t.push(a));return t.length?(t.includes("name")||t.unshift("name"),t):[...s]}export{l as a,r as b,y as c,p as d,d as e,c as f,s as g,g as h}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VFPFVFKO.js b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VFPFVFKO.js new file mode 100644 index 00000000..eb73bf71 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VFPFVFKO.js @@ -0,0 +1,2 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{q as _}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";function u(e){return`${e.entry_id??""}\0${e.part_id}`}function l(e,r,s,c){let n=!!e.entry_id&&e.entry_id!==r,o=n?e.entry_id:r,a=s.find(p=>p.entry_id===o),t=(a?.parts||[]).find(p=>p.id===e.part_id)||null,d=n&&a?.object?.name||"",i=t?.name||_("shared_part_unknown",c);return{part:t,foreign:n,ownerName:d,label:d?`${i} (${d})`:i}}function P(e,r,s,c){let{part:n,label:o}=l(e,r,s,c),a=n&&n.stock!==null&&n.stock!==void 0?` (${n.stock}${n.unit?" "+n.unit:""})`:"",t=n?.storage_location?` \u2014 ${n.storage_location}`:"";return`${e.quantity}\xD7 ${o}${a}${t}`}function g(e,r,s,c){let o=(s.find(t=>t.entry_id===r)?.parts||[]).map(t=>({...t})),a=new Set(o.map(t=>u({part_id:t.id})));for(let t of e?.consumes_parts||[]){if(!t.entry_id||t.entry_id===r)continue;let d=u(t);if(a.has(d))continue;a.add(d);let{part:i,ownerName:p}=l(t,r,s,c);o.push({id:t.part_id,name:i?.name||_("shared_part_unknown",c),unit:i?.unit,stock:i?.stock??null,storage_location:i?.storage_location,entry_id:t.entry_id,owner_name:p})}return o}var f=["notes","cost","duration","photo","user"],m={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};export{u as a,P as b,g as c,f as d,m as e}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VSV22GFT.js b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VSV22GFT.js new file mode 100644 index 00000000..fe02a7fa --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VSV22GFT.js @@ -0,0 +1,1193 @@ +/*! maintenance_supporter frontend 2.70.0 */ +var Re=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var it=(o,e,t,r)=>{for(var a=r>1?void 0:r?Oe(e,t):e,i=o.length-1,n;i>=0;i--)(n=o[i])&&(a=(r?n(e,t,a):n(a))||a);return r&&a&&Re(e,t,a),a};var M=globalThis,U=M.ShadowRoot&&(M.ShadyCSS===void 0||M.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,J=Symbol(),le=new WeakMap,E=class{constructor(e,t,r){if(this._$cssResult$=!0,r!==J)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(U&&e===void 0){let r=t!==void 0&&t.length===1;r&&(e=le.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&le.set(t,e))}return e}toString(){return this.cssText}},ce=o=>new E(typeof o=="string"?o:o+"",void 0,J),q=(o,...e)=>{let t=o.length===1?o[0]:e.reduce((r,a,i)=>r+(n=>{if(n._$cssResult$===!0)return n.cssText;if(typeof n=="number")return n;throw Error("Value passed to 'css' function must be a 'css' function result: "+n+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(a)+o[i+1],o[0]);return new E(t,o,J)},de=(o,e)=>{if(U)o.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let r=document.createElement("style"),a=M.litNonce;a!==void 0&&r.setAttribute("nonce",a),r.textContent=t.cssText,o.appendChild(r)}},Z=U?o=>o:o=>o instanceof CSSStyleSheet?(e=>{let t="";for(let r of e.cssRules)t+=r.cssText;return ce(t)})(o):o;var{is:De,defineProperty:ze,getOwnPropertyDescriptor:Le,getOwnPropertyNames:Me,getOwnPropertySymbols:Ue,getPrototypeOf:qe}=Object,I=globalThis,pe=I.trustedTypes,Ie=pe?pe.emptyScript:"",He=I.reactiveElementPolyfillSupport,T=(o,e)=>o,N={toAttribute(o,e){switch(e){case Boolean:o=o?Ie:null;break;case Object:case Array:o=o==null?o:JSON.stringify(o)}return o},fromAttribute(o,e){let t=o;switch(e){case Boolean:t=o!==null;break;case Number:t=o===null?null:Number(o);break;case Object:case Array:try{t=JSON.parse(o)}catch{t=null}}return t}},H=(o,e)=>!De(o,e),_e={attribute:!0,type:String,converter:N,reflect:!1,useDefault:!1,hasChanged:H};Symbol.metadata??=Symbol("metadata"),I.litPropertyMetadata??=new WeakMap;var g=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=_e){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let r=Symbol(),a=this.getPropertyDescriptor(e,r,t);a!==void 0&&ze(this.prototype,e,a)}}static getPropertyDescriptor(e,t,r){let{get:a,set:i}=Le(this.prototype,e)??{get(){return this[t]},set(n){this[t]=n}};return{get:a,set(n){let l=a?.call(this);i?.call(this,n),this.requestUpdate(e,l,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??_e}static _$Ei(){if(this.hasOwnProperty(T("elementProperties")))return;let e=qe(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(T("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(T("properties"))){let t=this.properties,r=[...Me(t),...Ue(t)];for(let a of r)this.createProperty(a,t[a])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[r,a]of t)this.elementProperties.set(r,a)}this._$Eh=new Map;for(let[t,r]of this.elementProperties){let a=this._$Eu(t,r);a!==void 0&&this._$Eh.set(a,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let r=new Set(e.flat(1/0).reverse());for(let a of r)t.unshift(Z(a))}else e!==void 0&&t.push(Z(e));return t}static _$Eu(e,t){let r=t.attribute;return r===!1?void 0:typeof r=="string"?r:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return de(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$ET(e,t){let r=this.constructor.elementProperties.get(e),a=this.constructor._$Eu(e,r);if(a!==void 0&&r.reflect===!0){let i=(r.converter?.toAttribute!==void 0?r.converter:N).toAttribute(t,r.type);this._$Em=e,i==null?this.removeAttribute(a):this.setAttribute(a,i),this._$Em=null}}_$AK(e,t){let r=this.constructor,a=r._$Eh.get(e);if(a!==void 0&&this._$Em!==a){let i=r.getPropertyOptions(a),n=typeof i.converter=="function"?{fromAttribute:i.converter}:i.converter?.fromAttribute!==void 0?i.converter:N;this._$Em=a;let l=n.fromAttribute(t,i.type);this[a]=l??this._$Ej?.get(a)??l,this._$Em=null}}requestUpdate(e,t,r,a=!1,i){if(e!==void 0){let n=this.constructor;if(a===!1&&(i=this[e]),r??=n.getPropertyOptions(e),!((r.hasChanged??H)(i,t)||r.useDefault&&r.reflect&&i===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,r))))return;this.C(e,t,r)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:r,reflect:a,wrapped:i},n){r&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,n??t??this[e]),i!==!0||n!==void 0)||(this._$AL.has(e)||(this.hasUpdated||r||(t=void 0),this._$AL.set(e,t)),a===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[a,i]of this._$Ep)this[a]=i;this._$Ep=void 0}let r=this.constructor.elementProperties;if(r.size>0)for(let[a,i]of r){let{wrapped:n}=i,l=this[a];n!==!0||this._$AL.has(a)||l===void 0||this.C(a,void 0,i,l)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(r=>r.hostUpdate?.()),this.update(t)):this._$EM()}catch(r){throw e=!1,this._$EM(),r}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};g.elementStyles=[],g.shadowRootOptions={mode:"open"},g[T("elementProperties")]=new Map,g[T("finalized")]=new Map,He?.({ReactiveElement:g}),(I.reactiveElementVersions??=[]).push("2.1.2");var ee=globalThis,ue=o=>o,F=ee.trustedTypes,he=F?F.createPolicy("lit-html",{createHTML:o=>o}):void 0,te="$lit$",m=`lit$${Math.random().toFixed(9).slice(2)}$`,oe="?"+m,Fe=`<${oe}>`,v=document,P=()=>v.createComment(""),R=o=>o===null||typeof o!="object"&&typeof o!="function",re=Array.isArray,ve=o=>re(o)||typeof o?.[Symbol.iterator]=="function",X=`[ +\f\r]`,j=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,ge=/-->/g,me=/>/g,b=RegExp(`>|${X}(?:([^\\s"'>=/]+)(${X}*=${X}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`,"g"),fe=/'/g,be=/"/g,xe=/^(?:script|style|textarea|title)$/i,ae=o=>(e,...t)=>({_$litType$:o,strings:e,values:t}),pt=ae(1),_t=ae(2),ut=ae(3),x=Symbol.for("lit-noChange"),p=Symbol.for("lit-nothing"),ye=new WeakMap,y=v.createTreeWalker(v,129);function we(o,e){if(!re(o)||!o.hasOwnProperty("raw"))throw Error("invalid template strings array");return he!==void 0?he.createHTML(e):e}var ke=(o,e)=>{let t=o.length-1,r=[],a,i=e===2?"":e===3?"":"",n=j;for(let l=0;l"?(n=a??j,c=-1):_[1]===void 0?c=-2:(c=n.lastIndex-_[2].length,d=_[1],n=_[3]===void 0?b:_[3]==='"'?be:fe):n===be||n===fe?n=b:n===ge||n===me?n=j:(n=b,a=void 0);let f=n===b&&o[l+1].startsWith("/>")?" ":"";i+=n===j?s+Fe:c>=0?(r.push(d),s.slice(0,c)+te+s.slice(c)+m+f):s+m+(c===-2?l:f)}return[we(o,i+(o[t]||"")+(e===2?"":e===3?"":"")),r]},O=class o{constructor({strings:e,_$litType$:t},r){let a;this.parts=[];let i=0,n=0,l=e.length-1,s=this.parts,[d,_]=ke(e,t);if(this.el=o.createElement(d,r),y.currentNode=this.el.content,t===2||t===3){let c=this.el.content.firstChild;c.replaceWith(...c.childNodes)}for(;(a=y.nextNode())!==null&&s.length0){a.textContent=F?F.emptyScript:"";for(let f=0;f2||r[0]!==""||r[1]!==""?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=p}_$AI(e,t=this,r,a){let i=this.strings,n=!1;if(i===void 0)e=w(this,e,t,0),n=!R(e)||e!==this._$AH&&e!==x,n&&(this._$AH=e);else{let l=e,s,d;for(e=i[0],s=0;s{let r=t?.renderBefore??e,a=r._$litPart$;if(a===void 0){let i=t?.renderBefore??null;r._$litPart$=a=new A(e.insertBefore(P(),i),i,void 0,t??{})}return a._$AI(o),a};var ie=globalThis,$=class extends g{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=Se(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return x}};$._$litElement$=!0,$.finalized=!0,ie.litElementHydrateSupport?.({LitElement:$});var We=ie.litElementPolyfillSupport;We?.({LitElement:$});(ie.litElementVersions??=[]).push("4.2.2");var ne="2.70.0";function Ct(o,e=ne){return!o||!e||e==="dev"?!1:o!==e}var Tt=o=>(e,t)=>{t!==void 0?t.addInitializer(()=>{customElements.define(o,e)}):customElements.define(o,e)};var Ve={attribute:!0,type:String,converter:N,reflect:!1,hasChanged:H},Ge=(o=Ve,e,t)=>{let{kind:r,metadata:a}=t,i=globalThis.litPropertyMetadata.get(a);if(i===void 0&&globalThis.litPropertyMetadata.set(a,i=new Map),r==="setter"&&((o=Object.create(o)).wrapped=!0),i.set(t.name,o),r==="accessor"){let{name:n}=t;return{set(l){let s=e.get.call(this);e.set.call(this,l),this.requestUpdate(n,s,o,!0,l)},init(l){return l!==void 0&&this.C(n,void 0,o,l),l}}}if(r==="setter"){let{name:n}=t;return function(l){let s=this[n];e.call(this,l),this.requestUpdate(n,s,o,!0,l)}}throw Error("Unsupported decorator location: "+r)};function Ae(o){return(e,t)=>typeof t=="object"?Ge(o,e,t):((r,a,i)=>{let n=a.hasOwnProperty(i);return a.constructor.createProperty(i,r),n?Object.getOwnPropertyDescriptor(a,i):void 0})(o,e,t)}function Ot(o){return Ae({...o,state:!0,attribute:!1})}var Ke={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},Qe={ok:"mdi:check-circle",due_soon:"mdi:alert-circle",overdue:"mdi:alert-octagon",triggered:"mdi:bell-alert",archived:"mdi:archive-outline",paused:"mdi:pause-circle-outline",completed:"mdi:check-circle",skipped:"mdi:skip-next",missed:"mdi:calendar-remove",reset:"mdi:refresh"};var $e={maintenance:"Maintenance",objects:"Objects",tasks:"Tasks",overdue:"Overdue",due_soon:"Due Soon",triggered:"Triggered",trigger_replaced:"Trigger replaced",trigger_removed:"Trigger removed",ok:"OK",all:"All",new_object:"+ New Object",templates_from:"From template",templates_title:"Start from a template",templates_task_count:"{n} tasks",template_created:"Created from template",onboard_hint:"Add your first object to start tracking maintenance.",edit:"Edit",duplicate:"Duplicate",task_duplicated:"Task duplicated",object_duplicated:"Object duplicated",delete:"Delete",add_task:"+ Add Task",complete:"Complete",completed:"Completed",skip:"Skip",skipped:"Skipped",missed:"Missed",reset:"Reset",snooze:"Snooze",snoozed:"Snoozed",cancel:"Cancel",bulk_select:"Select",bulk_select_all:"Select all",bulk_n_selected:"{n} selected",bulk_completed:"{n} tasks completed",bulk_archived:"{n} tasks archived",completing:"Completing\u2026",interval:"Interval",warning:"Warning",last_performed:"Last performed",next_due:"Next due",days_until_due:"Days until due",avg_duration:"Avg duration",trigger:"Trigger",trigger_type:"Trigger type",threshold_above:"Upper limit",threshold_below:"Lower limit",threshold:"Threshold",counter:"Counter",state_change:"State change",runtime:"Runtime",runtime_hours:"Target runtime (hours)",target_value:"Target value",baseline:"Baseline",target_changes:"Target changes",for_minutes:"For (minutes)",time_based:"Time-based",sensor_based:"Sensor-based",manual:"Manual",one_time:"One-time",weekdays:"Weekdays",nth_weekday:"Nth weekday of month",day_of_month:"Day of month",recurrence_on_days:"Repeat on",recurrence_occurrence:"Occurrence",recurrence_weekday:"Weekday",recurrence_day:"Day of month (1\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N days, e.g. -2 = two days before.",last_day_month:"Last day of month",last_business_day_month:"Last business day",ord_1:"1st",ord_2:"2nd",ord_3:"3rd",ord_4:"4th",ord_5:"5th",ord_last:"Last",day_word:"Day",interval_value:"Interval",interval_unit:"Unit",unit_days:"Days",unit_weeks:"Weeks",unit_months:"Months",unit_years:"Years",due_date:"Due date",cleaning:"Cleaning",inspection:"Inspection",replacement:"Replacement",calibration:"Calibration",service:"Service",reading:"Reading",custom:"Custom",history:"History",cost:"Cost",report_button:"Report",report_title:"Maintenance report",report_generated:"Generated",report_times_done:"Done",report_total_cost:"Total cost",report_every:"every {n} {unit}",report_notes:"Notes",report_col_type:"Type",report_col_status:"Status",report_col_schedule:"Schedule",duration:"Duration",both:"Both",trigger_val:"Trigger value",complete_title:"Complete: ",checklist:"Checklist",require_on_completion:"Require on completion",checklist_steps_optional:"Checklist steps (optional)",checklist_placeholder:`Clean filter +Replace seal +Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"{field}: too long (max {n} characters)",err_too_short:"{field}: too short (min {n} characters)",err_value_too_high:"{field}: too large (max {n})",err_value_too_low:"{field}: too small (min {n})",err_required:"{field}: required",err_wrong_type:"{field}: wrong type (expected: {type})",err_invalid_choice:"{field}: not an allowed value",err_invalid_value:"{field}: invalid value",feat_schedule_time:"Time-of-day scheduling",feat_schedule_time_desc:"Tasks become overdue at a specific time of day instead of midnight.",schedule_time_optional:"Due at time (optional, HH:MM)",schedule_time_help:"Empty = midnight (default). HA timezone.",at_time:"at",notes_optional:"Notes (optional)",notes_markdown_hint:"Markdown is supported \u2014 **bold**, lists, [links](\u2026)",cost_optional:"Cost (optional)",duration_minutes:"Duration in minutes (optional)",completed_at_optional:"Completed at (optional, empty = now)",completed_at_future_error:"The completion date cannot be in the future.",days:"days",day:"day",today:"Today",d_overdue:"d overdue",no_tasks:"No maintenance tasks yet. Create an object to get started.",no_tasks_short:"No tasks",no_history:"No history entries yet.",show_all:"Show all",cost_duration_chart:"Cost & Duration",installed:"Installed",confirm_delete_object:"Delete this object and all its tasks?",confirm_delete_task:"Delete this task?",min:"Min",max:"Max",save:"Save",saving:"Saving\u2026",edit_task:"Edit Task",new_task:"New Maintenance Task",task_name:"Task name",maintenance_type:"Maintenance type",priority:"Priority",labels:"Labels",labels_placeholder:"e.g. safety, seasonal, tenant-visible",labels_help:"Comma-separated tags for filtering and reporting.",priority_low:"Low",priority_normal:"Normal",priority_high:"High",all_priorities:"All priorities",schedule_type:"Schedule type",interval_days:"Interval (days)",warning_days:"Warning days",earliest_completion_days:"Earliest completion (days before due)",earliest_completion_days_help:"Leave empty to allow completing any time. 0 = only on/after the due date.",last_performed_optional:"Last performed (optional)",interval_anchor:"Interval anchor",anchor_completion:"From completion date",anchor_planned:"From planned date (no drift)",edit_object:"Edit Object",name:"Name",manufacturer_optional:"Manufacturer (optional)",model_optional:"Model (optional)",serial_number_optional:"Serial number (optional)",serial_number_label:"S/N",documentation_url_label:"Manual",object_notes_label:"Notes",sort_due_date:"Due date",sort_object:"Object name",sort_type:"Type",sort_task_name:"Task name",all_objects:"All objects",all_parts:"All parts",tasks_lower:"tasks",no_tasks_yet:"No tasks yet",add_first_task:"Add first task",trigger_configuration:"Trigger Configuration",entity_id:"Entity ID",comma_separated:"comma-separated",entity_logic:"Entity logic",entity_logic_any:"Any entity triggers",entity_logic_all:"All entities must trigger",entities:"entities",attribute_optional:"Attribute (optional, blank = state)",use_entity_state:"Use entity state (no attribute)",trigger_above:"Trigger above",trigger_below:"Trigger below",trigger_equals:"Trigger when equal to (=)",trigger_not_equals:"Trigger when different from (\u2260)",for_at_least_minutes:"For at least (minutes)",safety_interval_days:"Safety interval (days, optional)",safety_interval:"Safety interval (optional)",trigger_combinator:"Combine trigger and interval",trigger_combinator_any:"Trigger or interval (whichever first)",trigger_combinator_all:"Trigger and interval (both required)",delta_mode:"Delta mode",from_state_optional:"From state (optional)",to_state_optional:"To state (optional)",documentation_url_optional:"Documentation URL (optional)",object_notes_optional:"Notes (optional)",nfc_tag_id_optional:"NFC Tag ID (optional)",nfc_tags_empty_help:"No NFC tags registered in Home Assistant yet.",nfc_tags_open_settings:"Open Tags settings",nfc_tags_refresh:"Refresh",environmental_entity_optional:"Environmental sensor (optional)",environmental_entity_helper:"e.g. sensor.outdoor_temperature \u2014 adjusts the interval based on environmental conditions",adaptive_prediction_enabled:"Enable sensor-driven predictions",adaptive_seasonal_enabled:"Enable seasonal awareness",adaptive_max_interval:"Maximum interval (days)",adaptive_min_interval:"Minimum interval (days)",adaptive_ewa_alpha:"Learning rate (alpha)",adaptive_enabled:"Enable adaptive scheduling",adaptive_section_title:"Adaptive Scheduling",environmental_attribute_optional:"Environmental attribute (optional)",nfc_tag_id:"NFC Tag ID",nfc_linked:"NFC tag linked",nfc_link_hint:"Click to link NFC tag",responsible_user:"Responsible User",shared_with:"Shared with (rotation)",shared_with_help:"Pick multiple people to share this task; the responsible person rotates on each completion.",rotation_strategy:"Rotation",rotation_none:"No rotation",rotation_round_robin:"Round-robin",rotation_least_completed:"Least completed",rotation_random:"Random",no_user_assigned:"(No user assigned)",all_users:"All Users",my_tasks:"My Tasks",tab_calendar:"Calendar",cal_no_events:"No maintenance",cal_window_7:"7 days",cal_window_14:"14 days",cal_window_30:"30 days",cal_window_365:"1 year",cal_every_n_days:"every {n} days",cal_source_time:"Time-based",cal_source_time_adaptive:"Time-based (adaptive)",cal_source_sensor:"Sensor-based",cal_predicted:"predicted",cal_confidence_high:"high confidence",cal_confidence_medium:"medium confidence",cal_confidence_low:"low confidence",budget_monthly:"Monthly budget",budget_yearly:"Yearly budget",groups:"Groups",new_group:"New group",edit_group:"Edit group",no_groups:"No groups yet",delete_group:"Delete group",delete_group_confirm:"Delete group '{name}'?",group_select_tasks:"Select tasks",group_name_required:"Name is required",description_optional:"Description (optional)",selected:"Selected",loading_chart:"Loading chart data...",hide_outliers:"Hide outliers (sensor glitches)",was_maintenance_needed:"Was this maintenance needed?",feedback_needed:"Needed",feedback_not_needed:"Not needed",feedback_not_sure:"Not sure",suggested_interval:"Suggested interval",apply_suggestion:"Apply",reanalyze:"Re-analyze",reanalyze_result:"New analysis",reanalyze_insufficient_data:"Not enough data to produce a recommendation",data_points:"data points",dismiss_suggestion:"Dismiss",confidence_low:"Low",confidence_medium:"Medium",confidence_high:"High",recommended:"recommended",seasonal_awareness:"Seasonal Awareness",edit_seasonal_overrides:"Edit seasonal factors",seasonal_overrides_title:"Seasonal factors (override)",seasonal_overrides_hint:"Factor per month (0.1\u20135.0). Empty = learned automatically.",seasonal_override_invalid:"Invalid value",seasonal_override_range:"Factor must be between 0.1 and 5.0",clear_all:"Clear all",seasonal_chart_title:"Seasonal Factors",seasonal_learned:"Learned",seasonal_manual:"Manual",month_jan:"Jan",month_feb:"Feb",month_mar:"Mar",month_apr:"Apr",month_may:"May",month_jun:"Jun",month_jul:"Jul",month_aug:"Aug",month_sep:"Sep",month_oct:"Oct",month_nov:"Nov",month_dec:"Dec",sensor_prediction:"Sensor Prediction",degradation_trend:"Trend",trend_rising:"Rising",trend_falling:"Falling",trend_stable:"Stable",trend_insufficient_data:"Insufficient data",days_until_threshold:"Days until threshold",threshold_exceeded:"Threshold exceeded",environmental_adjustment:"Environmental factor",sensor_prediction_urgency:"Sensor predicts threshold in ~{days} days",day_short:"day",weibull_reliability_curve:"Reliability Curve",weibull_failure_probability:"Failure Probability",weibull_r_squared:"Fit R\xB2",beta_early_failures:"Early Failures",beta_random_failures:"Random Failures",beta_wear_out:"Wear-out",beta_highly_predictable:"Highly Predictable",confidence_interval:"Confidence Interval",confidence_conservative:"Conservative",confidence_aggressive:"Optimistic",current_interval_marker:"Current interval",recommended_marker:"Recommended",characteristic_life:"Characteristic life",chart_mini_sparkline:"Trend sparkline",chart_history:"Cost and duration history",chart_seasonal:"Seasonal factors, 12 months",chart_weibull:"Weibull reliability curve",chart_sparkline:"Sensor trigger value chart",days_progress:"Days progress",qr_code:"QR Code",qr_generating:"Generating QR code\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 Network.",save_error:"Failed to save. Please try again.",qr_print:"Print",qr_download:"Download SVG",qr_action:"Action on scan",qr_action_view:"View maintenance info",qr_action_complete:"Mark maintenance as complete",qr_url_mode:"Link type",qr_mode_companion:"Companion App",qr_mode_local:"Local (mDNS)",qr_mode_server:"Server URL",overview:"Overview",analysis:"Analysis",recent_activities:"Recent Activities",search_notes:"Search notes",avg_cost:"Avg Cost",no_advanced_features:"No advanced features enabled",no_advanced_features_hint:"Enable \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D in the integration settings to see analysis data here.",analysis_not_enough_data:"Not enough data for analysis yet.",analysis_not_enough_data_hint:"Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.",analysis_manual_task_hint:"Manual tasks without an interval do not generate analysis data.",completions:"completions",current:"Current",shorter:"Shorter",longer:"Longer",normal:"Normal",disabled:"Disabled",compound_logic:"Compound logic",compound:"Compound (multiple conditions)",compound_logic_and:"AND \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 add at least one.",compound_add_condition:"Add condition",compound_condition:"Condition",compound_remove_condition:"Remove condition",card_title:"Title",card_show_header:"Show header with statistics",card_show_actions:"Show action buttons",card_action_style:"Complete button style",card_compact:"Compact mode",card_max_items:"Max items (0 = all)",card_filter_status:"Filter by status",card_filter_status_help:"Empty = show all statuses.",card_filter_objects:"Filter by objects",card_filter_objects_help:"Empty = show all objects.",card_filter_areas:"Filter by areas",card_filter_areas_help:"Empty = show all areas.",card_filter_priority_help:"Empty = show all priorities. Tasks without an explicit priority count as Normal.",card_filter_entities:"Filter by entities (entity_ids)",card_filter_entities_help:"Pick sensor / binary_sensor entities from this integration. Empty = all.",card_loading_objects:"Loading objects\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 Create one in the Maintenance panel",no_objects:"No objects yet.",action_error:"Action failed. Please try again.",area_id_optional:"Area (optional)",installation_date_optional:"Installation date (optional)",warranty_expiry_optional:"Warranty expiry (optional)",warranty:"Warranty",warranty_valid_until:"valid until {date}",warranty_expires_in:"expires in {days} days",warranty_expired:"expired",cal_past_windows:"Past windows",cal_forward_windows:"Forward windows",history_edit_title:"Edit history entry",history_edit_timestamp:"Timestamp",manufacturer:"Manufacturer",model:"Model",area:"Area",actions:"Actions",view_mode_label:"View",view_cards:"Card view",view_table:"Table view",objects_table_columns_label:"Objects table columns",objects_table_columns_hint:"Choose which columns appear in the objects table view.",custom_icon_optional:"Icon (optional, e.g. mdi:wrench)",task_enabled:"Task enabled",skip_reason_prompt:"Skip this task?",reason_optional:"Reason (optional)",reset_date_prompt:"Mark task as performed?",reset_date_optional:"Last performed date (optional, defaults to today)",notes_label:"Notes",documentation_label:"Documentation",no_nfc_tag:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 Esc to close",today_all_caught_up:"All caught up! Nothing due this week.",today_overdue:"Overdue",today_due_today:"Due today",today_this_week:"This week",settings:"Settings",settings_features:"Advanced Features",settings_features_desc:"Enable or disable advanced features. Disabling hides them from the UI but does not delete data.",feat_adaptive:"Adaptive Scheduling",feat_adaptive_desc:"Learn optimal intervals from maintenance history",feat_predictions:"Sensor Predictions",feat_predictions_desc:"Predict trigger dates from sensor degradation",feat_seasonal:"Seasonal Adjustments",feat_seasonal_desc:"Adjust intervals based on seasonal patterns",feat_environmental:"Environmental Correlation",feat_environmental_desc:"Correlate intervals with temperature/humidity",feat_budget:"Budget Tracking",feat_budget_desc:"Track monthly and yearly maintenance spending",feat_groups:"Task Groups",feat_groups_desc:"Organize tasks into logical groups",feat_checklists:"Checklists",feat_checklists_desc:"Multi-step procedures for task completion",settings_general:"General",settings_default_warning:"Default warning days",settings_consumable_threshold:"Consumable low threshold (%)",settings_battery_low_percent:"Battery low threshold (%)",settings_thresholds_hint:"Defaults for new Suggested setups (percent-remaining consumables) and for fleet batteries without their own Battery Notes threshold. Existing tasks keep their value.",bn_summary:"{name}: {pct} % for {n} batteries",bn_floor_decides:"Your {floor} % floor decides for all of them; the higher of the two thresholds counts per battery.",bn_above_floor:"That is above your {floor} % floor, so {name} decides for all noted batteries.",bn_overrides:"{n} devices with their own threshold:",bn_more:"+ {n} more",settings_row_actions:"Task row actions",row_actions_buttons_compact:"Buttons (icons only on phones)",row_actions_buttons:"Buttons with text",row_actions_icons:"Icons (classic)",row_actions_follow:"Follow the household setting",settings_panel_enabled:"Sidebar panel",settings_panel_title:"Sidebar panel title",settings_notifications:"Notifications",settings_notify_service:"Notification service",settings_shopping_list:"Shopping list (buy tasks)",settings_shopping_list_help:"Low-part buy reminders appear in this to-do list; checking one off restocks the part.",shopping_list_none:"Off \u2014 no shopping list",settings_install_assist_sentences:"Install Assist sentences",settings_install_assist_sentences_hint:"Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",notify_per_person:"Per-person delivery",notify_no_own_device:"No own device \u2014 uses the household service",settings_notify_due_soon:"Notify when due soon",settings_notify_overdue:"Notify when overdue",settings_notify_triggered:"Notify when triggered",settings_interval_hours:"Repeat interval (hours, 0 = once)",settings_quiet_hours:"Quiet hours",settings_quiet_start:"Start",settings_quiet_end:"End",settings_max_per_day:"Max notifications per day (0 = unlimited)",settings_bundling:"Bundle notifications",settings_bundle_threshold:"Bundle threshold",settings_reminder_leads:"Extra reminders (days before due)",settings_reminder_leads_hint:"Comma-separated lead times, e.g. 14, 3, 0 \u2014 one extra reminder fires on each matching day. Empty = off.",settings_actions:"Mobile Action Buttons",settings_action_complete:"Show 'Complete' button",settings_action_skip:"Show 'Skip' button",settings_action_snooze:"Show 'Snooze' button",settings_weekly_digest:"Weekly digest",settings_weekly_digest_hint:"A single summary notification on Monday morning when tasks are due.",settings_warranty_reminder:"Warranty expiry reminder",settings_warranty_reminder_days:"Days before expiry",settings_warranty_reminder_hint:"Notify once when an object's warranty is this many days from expiring.",settings_snooze_hours:"Snooze duration (hours)",settings_budget:"Budget",settings_currency:"Currency",settings_budget_monthly:"Monthly budget",settings_budget_yearly:"Yearly budget",settings_budget_alerts:"Budget alerts",settings_budget_threshold:"Alert threshold (%)",settings_import_export:"Import / Export",settings_export_json:"Export JSON",settings_export_yaml:"Export YAML",settings_export_csv:"Export CSV",settings_export_settings:"Export settings (JSON)",settings_import_csv:"Import CSV",settings_import_placeholder:"Paste JSON or CSV content here\u2026",settings_import_btn:"Import",settings_import_success:"{count} objects imported successfully.",settings_export_success:"Export downloaded.",settings_saved:"Setting saved.",settings_include_history:"Include history",settings_export_selection:"Limit to selected objects (optional)",settings_docs_archive:"Documents archive (with files)",settings_docs_archive_hint:"The JSON/YAML/CSV exports carry settings only. This ZIP includes the uploaded file contents so a restore is complete.",settings_docs_export_btn:"Download documents ZIP",settings_docs_import_btn:"Restore documents ZIP",settings_docs_import_success:"Restored: {blobs} files, {docs} documents",sort_alphabetical:"Alphabetical",sort_due_soonest:"Due soonest",sort_task_count:"Task count",sort_area:"Area",sort_assigned_user:"Assigned user",sort_group:"Group",groupby_none:"No grouping",groupby_area:"By area",groupby_group:"By group",groupby_user:"By user",filter_label:"Filter",user_label:"User",photo_label:"Photo",sort_label:"Sort",group_by_label:"Group by",state_value_help:'Use the HA state value (usually lowercase, e.g. "on"/"off"). Case is normalised on save.',target_changes_help:"Number of matching transitions before the trigger fires (default: 1).",for_minutes_state_help:"0 counts every change immediately. Set minutes and the new state must hold that long first \u2014 brief flickers then neither trigger nor count.",qr_print_title:"Print QR codes",qr_print_desc:"Generate a printable page of QR codes to cut out and stick on your equipment.",qr_print_load:"Load objects",qr_print_filter:"Filter",qr_print_objects:"Objects",qr_print_actions:"Actions",qr_print_url_mode:"Link type",qr_print_estimate:"Estimated QR codes",qr_print_over_limit:"cap is 200, narrow the filter",qr_print_generate:"Generate QR codes",qr_print_generating:"Generating\u2026",qr_print_ready:"QR codes ready",qr_print_print_button:"Print",qr_print_empty:"Nothing to generate",qr_action_skip:"Skip",vacation_title:"Vacation mode",vacation_active:"active",vacation_ended:"ended",vacation_desc:"Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.",vacation_enable:"Enable vacation mode",vacation_start:"Start",vacation_end:"End",vacation_buffer:"Buffer (days)",vacation_exempt_title:"Notify anyway during vacation",vacation_exempt_desc:"Pick tasks that should still notify during vacation (e.g. critical pool chemistry).",vacation_load_tasks:"Load tasks",vacation_preview_btn:"Show preview",vacation_preview_affected:"tasks affected",vacation_event_due_soon:"becomes due soon",vacation_event_overdue:"becomes overdue",vacation_event_triggered_est:"sensor trigger possible",vacation_sensor_based:"(sensor-based)",vacation_action_notify:"Notify anyway",vacation_action_unsilence:"Silence again",vacation_marked_complete:"Marked complete",vacation_marked_skip:"Skipped",vacation_end_now:"End vacation now",add:"Add",show_stats:"Show stats + graphs",hide_stats:"Hide stats",adaptive_no_data:"Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.",suggestion_applied:"Suggested interval applied",vacation_mode:"Vacation mode",vacation_status_active:"Active now",vacation_status_scheduled:"Scheduled",vacation_status_inactive:"Inactive",vacation_end_now_confirm:"End vacation immediately?",vacation_exempt_count:"exempt",vacation_advanced:"Advanced\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",groups_open_panel:"Open in panel",unassigned:"Unassigned",no_area:"No area",has_overdue:"Has overdue tasks",object:"Object",settings_panel_access:"Panel access",settings_panel_access_desc:"Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below \u2014 everyone else sees only Complete and Skip.",settings_operator_write:"Allow selected users to create, edit & delete",settings_operator_write_desc:"Off: only admins can change content. On: the selected users below get full access too.",no_non_admin_users:"No non-admin users found. Add some in Settings \u2192 People.",owner_label:"Owner",feat_completion_actions:"Completion actions",feat_completion_actions_desc:"Per-task HA action on complete + quick-complete QR with pre-set values.",on_complete_action_title:"On complete: trigger HA action (optional)",on_complete_action_desc:"Calls an HA service when the task is completed \u2014 e.g. reset a counter on the device.",on_complete_action_service:"Service",on_complete_action_target:"Target entity",on_complete_action_target_hint:"Note: the entity domain must match the service \u2014 e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').",on_complete_action_data:"Data (JSON, optional)",on_complete_action_test:"Validate configuration",on_complete_action_test_success:"\u2713 Configuration valid (action will fire only on task completion)",on_complete_action_test_failed:"Failed",quick_complete_defaults_title:"Quick-complete defaults (for QR scans, optional)",quick_complete_defaults_desc:"Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.",quick_complete_defaults_notes:"Notes",quick_complete_defaults_cost:"Cost",quick_complete_defaults_duration:"Duration (minutes)",quick_complete_defaults_feedback_none:"No feedback",quick_complete_defaults_feedback_needed:"Was needed",quick_complete_defaults_feedback_not_needed:"Not needed",quick_complete_success:"Quickly marked complete",show_all_objects:"Show all objects",show_all_tasks:"Clear filter \u2014 show all tasks",filter_to_overdue:"Filter task list to overdue only",filter_to_due_soon:"Filter task list to due-soon only",filter_to_triggered:"Filter task list to triggered only",open_task:"Open task",show_details:"Show history + stats",hide_details:"Hide details",history_empty:"No history yet.",history_edit_button:"Edit entry",total_cost:"Total cost",times_performed:"Performed",older_entries:"older",open_in_panel:"Open in Maintenance panel",skip_reason:"Skip reason (optional)",reset_to_date:"Reset last_performed to",delete_task_confirm:"Delete this task and its history?",delete_object_confirm:"Delete this object and all its tasks?",loading:"Loading\u2026",archive:"Archive",undo:"Undo",task_archived:"Task archived",object_archived:"Object archived",unarchive:"Unarchive",archived:"Archived",show_archived:"Show archived",hide_archived:"Hide archived",confirm_archive_object:"Archive this object and its tasks? They keep their history and can be unarchived later.",settings_archive:"Archive & Retention",settings_archive_desc:"Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.",settings_archive_oneoff_days:"Auto-archive completed one-off tasks after (days, 0 = off)",settings_delete_archived_oneoff_days:"Auto-delete archived one-off tasks after (days, 0 = never)",archive_object:"Archive object",unarchive_object:"Unarchive object",documents:"Documents",documents_empty:"No documents yet.",doc_upload:"Upload file",doc_uploading:"Uploading\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",doc_link_title:"Title (optional)",doc_open:"Open",doc_delete_confirm:'Delete "{name}"?',doc_too_large:"File is too large (max 25 MB).",doc_upload_failed:"Upload failed.",completion_photo_optional:"Completion photo (optional)",add_photo:"Add photo",uploading:"Uploading\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 shared, no extra space used.",doc_dup_in_object:"This file is already attached to this object.",doc_link_invalid:"Only http/https links are allowed.",doc_cat_manual:"Manual",doc_cat_warranty:"Warranty",doc_cat_invoice:"Invoice",doc_cat_spare_parts:"Spare parts",doc_cat_photo:"Photo",doc_cat_other:"Other",doc_link_badge:"Link",doc_storage_title:"Document storage",doc_storage_saved:"Saved via deduplication",doc_storage_refresh:"Refresh",doc_download:"Download",doc_close:"Close",doc_camera:"Take photo",doc_drop_hint:"Drop files here",doc_task_none:"No documents linked to this task.",doc_link_existing:"Link a document\u2026",doc_attach:"Link",doc_unlink:"Unlink",doc_page:"Page",chart_range_7d:"7d",chart_range_30d:"30d",chart_range_90d:"90d",chart_range_1y:"1y",chart_since_service:"since last service",chart_no_stats:"No long-term statistics for this entity \u2014 showing maintenance-event values only",auto_complete_on_recovery:"Auto-complete when the sensor recovers",auto_complete_on_recovery_help:"Records a completion (sets last performed) when the trigger clears itself \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",doc_search_none:"No matching documents",link_device_optional:"Link to existing device (optional)",parent_object_optional:"Parent object (optional)",parent_none:"(No parent)",paused:"Paused",pause_object:"Pause",resume_object:"Resume",pause_until_prompt:"Freeze this object's schedules \u2014 nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.",pause_until_label:"Resume on (optional)",object_paused:"Object paused",object_resumed:"Object resumed \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",replace_object_prompt:"Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.",replace_name_label:"Successor name",object_replaced:"Object replaced \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",reading_unit_help:"Shown next to the recorded value when completing this task.",reading_value_label:"Reading value",reading_label:"Reading",settings_templates_label:"Template gallery",settings_templates_hint:`Untick templates you'll never need \u2014 they disappear from the "From template" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.`,worksheet:"Work sheet",worksheet_scan_view:"Scan to open the task",worksheet_scan_complete:"Scan to complete",worksheet_manual_excerpt:"Manual excerpt",worksheet_pages:"pages",worksheet_printed:"Printed",worksheet_never:"Never",card_all_caught_up:"All caught up \u2014 nothing needs attention",postpone:"Postpone",postpone_date_prompt:"Postpone this occurrence to which date?",postpone_date_label:"New due date",postponed:"Postponed",postponed_to:"Postponed to",season_window_label:"Seasonal window (months)",season_window_hint:"Only due in the selected months; off-season dates roll to the next active month. None = all year.",series_end_label:"Ends",series_end_never:"Never (repeats indefinitely)",series_end_after_count:"After a number of times",series_end_until:"On a date",series_end_count_label:"Number of times",series_end_until_label:"End date",parts_section:"Parts & consumables",parts_inventory_value:"Inventory value",part_add:"Add part",part_name:"Name",part_vendor:"Manufacturer",part_storage_location:"Storage location",part_product_url:"Product URL",part_unit:"Unit",part_cost:"Unit price",part_stock:"Stock",part_reorder_threshold:"Reorder at",part_restock_quantity:"Restock quantity",part_auto_buy:"Auto-create buy task when low",part_restock:"Adjust stock",parts_used_by:"Used by",restock_quantity_label:"Quantity bought",consumes_parts_label:"Consumes parts",shared_parts_other_objects:"Parts from other objects",shared_parts_help:"Several objects can share one stock. Completing this task takes from the owning object.",shared_part_unknown:"Unknown part",parts_load_failed:"Couldn't load this object's parts \u2014 the consumes-parts options are unavailable right now.",adopt_problem_button:"Adopt problem sensors",adopt_problem_title:"Adopt problem sensors",adopt_problem_hint:"Turn HA problem sensors (printer errors, filter warnings, low battery) into maintenance tasks that trigger while the problem is active and clear themselves when it resolves.",adopt_problem_none:"No problem sensors found that aren't already tracked.",adopt_problem_active:"active",adopt_problem_ok:"ok",adopt_problem_new_object:"(new)",adopt_problem_adopt:"Adopt selected",adopt_problem_done:"Adopted {tasks} problem sensor(s)",views_label:"Views",views_none:"\u2014 No view \u2014",views_manage:"Save / manage views",views_dialog_title:"Saved views",views_dialog_hint:"Save the current filters as a named view everyone can reuse.",views_name_placeholder:"View name",views_save_current:"Save current filters",views_none_yet:"No saved views yet.",close:"Close",trigger_hint_now:"The sensor reads {value} right now.",trigger_hint_above:"The task triggers once it rises above {target}.",trigger_hint_below:"It triggers once it falls below {target}.",trigger_hint_counter_delta:"Counts from the current reading ({value}): due at {due} (+{target}), and the count restarts after each completion.",trigger_hint_counter_delta_edit:"Counts usage since the last completion: due after +{target}; the count restarts after each completion.",trigger_hint_counter_abs:"The task becomes due once the sensor reaches {target}.",trigger_hint_runtime:"The task becomes due after {hours} h of accumulated on-time; the counter restarts after each completion.",trigger_hint_state_change:"The task becomes due after {count} state change(s).",trigger_hint_state_change_to:"The task becomes due after {count} change(s) to \u201C{state}\u201D.",trigger_hint_state_now:"Current state: {value}.",adopt_problem_part:"Uses part: {name}",label_filter:"Label",all_labels:"All labels",settings_notify_scope:"Notify only for view",settings_notify_scope_all:"All tasks",settings_notify_scope_hint:"Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",card_saved_view:"Saved view",card_saved_view_none:"None",card_saved_view_help:"Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",doc_part_none:"No documents linked to this part.",settings_templates_toggle_group:"Enable or disable all templates in this group",setups_button:"Suggested setups",setups_title:"Suggested setups (Beta)",setups_hint:"Devices of supported integrations whose consumable sensors can drive maintenance tasks. Adopting creates the object and wires each task to its sensor \u2014 it triggers when the consumable runs low and resolves itself after replacement.",setups_none:"No supported devices with unwired consumable sensors found.",setups_adopt:"Set up selected",setups_done:"{tasks} sensor-wired tasks created.",complete_parts_used:"Parts used this time",part_delete_confirm:"Delete part '{name}'? Its stock tracking, task links and any open buy reminder will be removed.",baseline_start_value:"Start reading (optional)",baseline_start_help:"Counting starts from this reading. Leave empty to count from the current value; enter the reading at the last service so usage since then already counts.",setups_baseline_hint:"reading at last service (optional)",baseline_start_help_edit:"Leave empty to keep the existing counting. Entering a value re-anchors the counting (e.g. the reading at the last service).",baseline_current_effective:"Currently effective start value: {value}",runtime_on_states:"Active states",runtime_on_states_help:"States that count as running \u2014 default: on. E.g. mowing, cleaning, printing. With an attribute selected, its values are matched instead.",setups_target_new:"Create new: {name}",schedule_preview_title:"Next dates",schedule_preview_ontime:"Assuming on-time completion.",schedule_preview_ends:"(series ends)",adopt_problem_responsible:"Responsible user for all adopted tasks (optional)",adopt_for_minutes_hint:"Only trigger once the problem has persisted this long \u2014 0 reacts to the first flicker.",adopt_problem_configure:"Configure",history_auto:"Automatic",battery_fleet_title:"Battery fleet",battery_fleet_none_low:"All batteries OK \u2014 nothing to replace.",battery_fleet_buy_now:"Buy now",battery_fleet_soon:"Needed soon",battery_fleet_soon_hint:"Predicted from the last replacement date \u2014 order ahead.",battery_fleet_mark_all:"Mark all replaced",battery_fleet_mark_one:"Mark this battery replaced",battery_fleet_offline:"offline",battery_fleet_trigger_lost:"This task's sensor trigger was lost \u2014 it will not fire or auto-complete.",battery_fleet_repair:"Repair",battery_fleet_exclude:"Exclude from the fleet",battery_fleet_excluded:"Excluded",battery_fleet_include:"Track again",battery_fleet_all:"All tracked batteries",battery_fleet_all_hint:"Exclude a device here to drop it from the fleet before it ever reports low \u2014 a vacuum that recharges itself, or a phone that warns you on its own.",battery_fleet_add:"Add a battery",battery_fleet_add_hint:"Pick a battery sensor the automatic discovery missed \u2014 it joins the roster immediately.",battery_fleet_track_self:"Track self-charging batteries",battery_fleet_track_self_hint:"Phones, vacuums and other devices that recharge themselves appear as rechargeables \u2014 a low one asks for a charge, never for new cells.",battery_fleet_status_low:"Low",battery_fleet_status_soon:"Soon",battery_fleet_status_ok:"Healthy",battery_fleet_predicted_on:"Expected around {date}",battery_fleet_predicted_trend:"Predicted from this battery's discharge trend: around {date} ({confidence})",battery_fleet_rechargeable:"Rechargeable: charge instead of replacing \u2014 never on the shopping list",battery_fleet_sort_name:"Sort by name",battery_fleet_sort_urgency:"Sort by urgency",battery_fleet_mark_recharged:"Mark as recharged",battery_fleet_sparkline_hint:"Battery level over the last 30 days \u2014 dotted: projected until the low threshold",battery_fleet_filter_type:"Show only this battery type",battery_fleet_record_replacement:"The level jumped around {date} \u2014 record this replacement in Battery Notes",battery_fleet_total:"{n} batteries tracked",battery_fleet_setup_button:"Battery fleet",battery_fleet_setup_done:"Battery fleet set up \u2014 one task tracks all your batteries.",update_banner:"A newer version of Maintenance Supporter is on the server \u2014 reload to update the panel.",update_reload:"Reload",row_actions_banner:"Complete and Skip in task rows are now buttons. Prefer the old icons?",row_actions_keep:"Keep buttons",row_actions_back:"Back to icons",battery_fleet_forecast_overdue:"Predicted date passed \u2014 the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",cost_from_parts:"Use \u2248 {amount} from parts",dismiss:"Dismiss",gs_label:"Getting started \u2014 these hints retire as your setup grows",gs_setups_chip:"Suggested setups found {n} devices with pre-wired triggers",gs_adopt_chip:"{n} problem sensors can become maintenance tasks",gs_fleet_chip:"One click sets up the battery fleet",cal_editor_window:"Default window",cal_editor_window_week:"Week (7 days)",cal_editor_window_fortnight:"Fortnight (14 days)",cal_editor_window_month:"Month (30 days, default)",cal_editor_window_year:"Year (365 days, empty days collapsed)",cal_editor_show_chips:"Show window chips inside the card",cal_editor_chips_hint:"Hide the chips when the card is embedded in a strategy view that already serves as the window selector.",cal_editor_show_user_filter:"Show user filter dropdown",cal_editor_default_user:"Default user filter",cal_editor_my_tasks:"My tasks (current user)",cal_editor_show_object_filter:"Show object filter dropdown",cal_editor_object_hint:'Pre-select one object via YAML: object_filter: "" \u2014 or a list of names to restrict the card to several objects.',object_history_section:"History (all tasks)",object_history_all_tasks:"All tasks",object_history_empty:"No entries in this range.",object_history_cap_note:"History keeps up to 500 entries per task \u2014 very old entries may be missing.",service_record_title:"Service record",service_record_print:"Service record (PDF)",date:"Date",service_record_entries:"entries",completed_by:"Completed by",date_from:"From",date_to:"To",phases_section:"Cycle phases (optional)",phases_hint:"Different work on one shared schedule \u2014 each completion moves to the next step (e.g. small service, small service, big service).",phase_add:"Add phase",phase_name:"Phase name",phase_sequence_label:"Cycle order",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 \u2014 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 \u201CRequire on completion\u201D 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 \u2014 saving here will be refused."};var co="\u20AC",se="en",Ne=(()=>{let o=window;return o.__msLocales||(o.__msLocales={store:{},inflight:{}}),o.__msLocales})(),S=Ne.store;function Je(o){S.en=Object.assign({},o,S.en??{})}Je($e);var Ze=new Set(["de","nl","fr","it","es","pt","pt-br","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi","hu","ko","tr"]),Xe="/maintenance_supporter_locales",D=Ne.inflight;function z(o){let e=(o||se).toLowerCase();return e.startsWith("pt")&&e.endsWith("br")?"pt-br":e.substring(0,2)}function u(o,e){let t=z(e);return S[t]?.[o]??S.en[o]??o}function po(o,e){e.has("hass")&&rt(o.hass?.locale,o.hass?.config?.country);let t=o.hass?.language;t&&!et(t)&&tt(t).then(()=>o.requestUpdate())}function _o(o){return o?.language||"en"}function et(o){let e=z(o);return e===se||e in S}function tt(o){let e=z(o);return e===se||e in S||!Ze.has(e)?Promise.resolve():(e in D||(D[e]=fetch(`${Xe}/${e}.json?v=${ne}`).then(t=>t.ok?t.json():null).then(t=>{t?S[e]=t:delete D[e]}).catch(()=>{delete D[e]})),D[e])}function je(o){let e=z(o);return{de:"de-DE",en:"en-US",nl:"nl-NL",fr:"fr-FR",it:"it-IT",es:"es-ES",pt:"pt-PT",ru:"ru-RU",uk:"uk-UA",zh:"zh-CN",da:"da-DK",fi:"fi-FI",nb:"nb-NO",ja:"ja-JP",hi:"hi-IN",pl:"pl-PL",cs:"cs-CZ",sv:"sv-SE","pt-br":"pt-BR",hu:"hu-HU",ko:"ko-KR",tr:"tr-TR"}[e]??"en-US"}var ot=window,C=ot.__msDateTimePrefs??={};function rt(o,e){o&&(C.date=o.date_format,C.time=o.time_format,e!==void 0&&(C.country=e||void 0))}function Y(o){let e=je(o),t=C.country;if(t&&/^[A-Za-z]{2}$/.test(t)){let r=`${z(o).split("-")[0]}-${t.toUpperCase()}`;try{return new Intl.DateTimeFormat(r),r}catch{}}return e}function Pe(o,e){let t=String(o.getDate()).padStart(2,"0"),r=String(o.getMonth()+1).padStart(2,"0"),a=String(o.getFullYear());switch(C.date){case"DMY":return`${t}/${r}/${a}`;case"MDY":return`${r}/${t}/${a}`;case"YMD":return`${a}-${r}-${t}`;case"system":return o.toLocaleDateString(void 0,{day:"2-digit",month:"2-digit",year:"numeric"});default:return o.toLocaleDateString(Y(e),{day:"2-digit",month:"2-digit",year:"numeric"})}}function at(o,e){switch(C.time){case"12":return o.toLocaleTimeString(Y(e),{hour:"2-digit",minute:"2-digit",hour12:!0});case"24":return o.toLocaleTimeString(Y(e),{hour:"2-digit",minute:"2-digit",hour12:!1});case"system":return o.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"});default:return o.toLocaleTimeString(Y(e),{hour:"2-digit",minute:"2-digit"})}}function Ce(o,e){if(!o)return"\u2014";try{let t=o.includes("T")?o:o+"T00:00:00";return Pe(new Date(t),e)}catch{return o}}function uo(o,e){if(!o)return"\u2014";try{let t=new Date(o);return Pe(t,e)+" "+at(t,e)}catch{return o}}function ho(o,e){if(o==null)return"\u2014";let t=e||"en";return o<0?`${Math.abs(o)} ${u("d_overdue",t)}`:o===0?u("today",t):`${o} ${u(o===1?"day":"days",t)}`}function Ee(o,e,t){return o==null?"\u2014":`${o} ${u("unit_"+(e||"days"),t)}`}function Te(o,e,t="long"){return new Date(Date.UTC(2024,0,1+o)).toLocaleDateString(je(e),{weekday:t,timeZone:"UTC"})}function go(o,e){let t=o.schedule,r=t?.offset?` ${t.offset>0?"+":"\u2212"}${Math.abs(t.offset)}d`:"";switch(t?.kind){case"weekdays":return((t.weekdays||[]).map(a=>Te(a,e,"short")).join(" & ")||"\u2014")+r;case"nth_weekday":return t.weekday==null||t.nth==null?"\u2014":`${t.nth===-1?u("ord_last",e):u("ord_"+t.nth,e)} ${Te(t.weekday,e,"long")}${r}`;case"day_of_month":return t.day==null?"\u2014":(t.day===-1?u(t.business?"last_business_day_month":"last_day_month",e):`${u("day_word",e)} ${t.day}`)+r;case"one_time":return o.due_date?Ce(o.due_date,e):u("one_time",e);case"manual":return u("manual",e);case"interval":return Ee(t.every,t.unit,e)}return o.schedule_type==="one_time"?o.due_date?Ce(o.due_date,e):u("one_time",e):o.schedule_type==="manual"?u("manual",e):o.schedule_type==="sensor_based"?u("sensor_based",e):o.interval_days!=null?Ee(o.interval_days,o.interval_unit,e):"\u2014"}function mo(o,e){o.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:e},bubbles:!0,composed:!0}))}var fo=q` + .field { display: flex; flex-direction: column; gap: 4px; } + .field-label { font-size: 12px; color: var(--secondary-text-color); } + .field-input { + 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); border-radius: 6px; + font-family: inherit; width: 100%; box-sizing: border-box; + } + .field-input:focus { outline: none; border-color: var(--primary-color); } +`,bo=q` + :host { + --maint-ok-color: var(--success-color, #4caf50); + --maint-due-soon-color: var(--warning-color, #ff9800); + --maint-overdue-color: var(--error-color, #f44336); + /* Theme-token first so it follows dark/custom themes (was a bare #ff5722, + inconsistent with STATUS_COLORS.triggered which already tokenised it). */ + --maint-triggered-color: var(--deep-orange-color, #ff5722); + } + + .status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + color: white; + white-space: nowrap; + /* Fixed minimum so OK / Due Soon / Overdue / Triggered pills are uniform + width in the task table — keeps the object-name column aligned. */ + min-width: 70px; + box-sizing: border-box; + } + /* Shape icon so status is not conveyed by colour alone (accessibility). */ + .status-badge ha-icon { --mdc-icon-size: 14px; margin-left: -1px; } + + /* Light-background statuses (green/orange/grey) carry DARK text: white on + them fails even the 3:1 WCAG UI-contrast floor (2.2–2.8:1), while the + saturated statuses below keep white (≥3.1:1). Matches the calendar pills. */ + .status-badge.ok { background-color: var(--maint-ok-color); color: #000; } + .status-badge.due_soon { background-color: var(--maint-due-soon-color); color: #000; } + .status-badge.overdue { background-color: var(--maint-overdue-color); } + .status-badge.triggered { background-color: var(--maint-triggered-color); } + /* Completed one-time task ("done") — muted blue-grey. */ + .status-badge.done { background-color: var(--maint-done-color, #78909c); } + /* v2.10.0: archived (retire-but-retain) — neutral grey, clearly inert. */ + .status-badge.archived { background-color: var(--disabled-color, #9e9e9e); color: #000; } + /* v2.20 (N3): paused — frozen but present, info blue. */ + .status-badge.paused { background-color: var(--info-color, #2196f3); } + + /* v1.4.7: 5-column grid so all 5 KPIs (Objects/Tasks/Overdue/Due Soon/ + Triggered) always stay in one row. The previous flex-wrap layout was + wrapping the 5th item (Triggered, the widest label) onto its own row + on narrow viewports because the natural width of the items pushed past + the container width. Grid forces equal 1/5 distribution regardless of + label length. */ + .stats-bar { + display: grid; + /* auto-fit instead of a fixed 5: with the budget feature on, two KPI + tiles join the strip (#125) — and on narrow screens the tiles wrap + instead of crushing. */ + grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); + gap: 16px; + padding: 16px; + } + + .stat-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 0; + } + .stat-item .stat-label { + /* Allow long labels to ellipsis rather than overflow the grid cell. */ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .stat-item.clickable { cursor: pointer; border-radius: 8px; padding: 4px 8px; transition: background 0.15s, box-shadow 0.15s; } + .stat-item.clickable:hover { background: var(--secondary-background-color); } + /* v2.1.0 — KPIs that map to a status filter highlight when active so the + user can see at a glance which filter is on, even after scrolling away. */ + .stat-item.clickable.active { + background: var(--secondary-background-color); + box-shadow: inset 0 -3px 0 var(--primary-color); + } + + .objects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; + padding: 16px 0; + } + .object-card { + padding: 16px; + background: var(--card-background-color); + border-radius: 8px; + cursor: pointer; + border: 1px solid var(--divider-color); + transition: transform 0.15s, box-shadow 0.15s; + /* Large installs (100+ objects): skip rendering off-screen cards. The + intrinsic size keeps the scrollbar stable while they're skipped. */ + content-visibility: auto; + contain-intrinsic-size: auto 120px; + } + .object-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } + .object-card-header { display: flex; justify-content: space-between; align-items: center; } + .object-card-name { font-weight: 500; font-size: 16px; } + .object-card-count { color: var(--secondary-text-color); font-size: 13px; } + .object-card-meta { color: var(--secondary-text-color); font-size: 13px; margin-top: 4px; } + .object-card-empty { color: var(--warning-color); font-size: 13px; margin-top: 8px; font-style: italic; } + + /* Overdue indicator dot on object cards (#35) */ + .object-card { position: relative; } + .object-card-overdue { border-left: 3px solid var(--error-color); } + .overdue-dot { + position: absolute; + top: 12px; + right: 12px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--error-color); + box-shadow: 0 0 0 2px var(--card-background-color); + } + + /* Group-by collapsible sections (#35 + #36) */ + .group-section { + margin: 12px 0; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color); + } + .group-section[open] { padding-bottom: 8px; } + .group-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + cursor: pointer; + font-weight: 500; + list-style: none; + user-select: none; + } + .group-section-header::-webkit-details-marker { display: none; } + .group-section-header::before { + content: "▶"; + font-size: 10px; + color: var(--secondary-text-color); + transition: transform 0.15s; + } + .group-section[open] .group-section-header::before { transform: rotate(90deg); } + .group-section-count { + color: var(--secondary-text-color); + font-size: 13px; + font-weight: 400; + } + .group-section .objects-grid, + .group-section .task-table { + padding: 0 12px; + } + + .empty-state-centered { text-align: center; padding: 32px 16px; } + .empty-state-centered ha-button { margin-top: 16px; } + + .stat-value { + font-size: 24px; + font-weight: bold; + color: var(--primary-text-color); + } + + .stat-label { + font-size: 12px; + color: var(--secondary-text-color); + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + } + + .card-header h1 { + margin: 0; + font-size: 20px; + font-weight: 500; + } + + .action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .action-buttons ha-button { + --ha-button-font-size: 13px; + } + + .history-timeline { padding: 0 16px 16px; } + + .history-entry { + display: flex; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + /* Long histories: skip painting off-screen entries (flex, not subgrid, so + safe — subgrid task rows can't use this without breaking alignment). */ + content-visibility: auto; + contain-intrinsic-size: auto 48px; + } + .history-entry:last-child { border-bottom: none; } + + .history-icon { + flex-shrink: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: white; + } + + .history-icon.completed { background: var(--maint-ok-color); } + .history-icon.skipped { background: var(--secondary-text-color); } + .history-icon.reset { background: var(--info-color, #2196f3); } + .history-icon.triggered { background: var(--maint-triggered-color); } + + .history-content { flex: 1; min-width: 0; } + + /* v2.2.0 — row holds the type label + the small Edit button */ + .history-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + /* v2.37 — marks completions the system recorded itself (trigger recovered). + margin-right:auto keeps it left beside the type label while the edit + button stays pinned right by the row's space-between. */ + .history-auto-badge { + margin-right: auto; + font-size: 11px; + padding: 1px 8px; + border-radius: 10px; + background: var(--secondary-background-color); + color: var(--secondary-text-color); + white-space: nowrap; + } + /* #139 — names the cycle phase a completion recorded. Shares the auto-badge + look; carries the left-pinning margin itself unless the auto badge (which + already has it) follows. */ + .history-phase-badge { + margin-right: auto; + font-size: 11px; + padding: 1px 8px; + border-radius: 10px; + background: var(--secondary-background-color); + color: var(--secondary-text-color); + white-space: nowrap; + } + .history-phase-badge:has(+ .history-auto-badge) { + margin-right: 0; + } + .history-edit-btn { + background: transparent; + color: var(--secondary-text-color); + border: none; + border-radius: 4px; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + transition: background 0.15s, color 0.15s; + } + .history-edit-btn:hover { + background: var(--secondary-background-color); + color: var(--primary-color); + } + .history-edit-btn ha-icon { --mdc-icon-size: 16px; } + + .history-date { + font-size: 12px; + color: var(--secondary-text-color); + } + + .history-details { + display: flex; + gap: 12px; + font-size: 13px; + color: var(--secondary-text-color); + margin-top: 4px; + } + + /* History filter chips */ + .history-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + } + + .filter-chip { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + cursor: pointer; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); + transition: all 0.2s; + user-select: none; + } + + .filter-chip:hover { background: var(--divider-color); } + + .filter-chip.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + .filter-chip.clear { + font-style: italic; + opacity: 0.7; + } + + /* Cost/Duration history chart */ + .history-chart { + width: 100%; + height: 200px; + display: block; + } + + .chart-legend { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 4px; + font-size: 11px; + color: var(--secondary-text-color); + } + + .legend-item { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .legend-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 16px; + color: var(--secondary-text-color); + } + + .empty-state ha-svg-icon { + --mdc-icon-size: 48px; + margin-bottom: 16px; + } + + /* Sparkline chart */ + .sparkline-container { position: relative; margin: 8px 0; } + + .sparkline-svg { + width: 100%; + height: 140px; + display: block; + } + + /* Trigger info card */ + .trigger-card { + background: var(--card-background-color, #fff); + border-radius: 12px; + padding: 12px 16px; + margin: 8px 0; + border: 1px solid var(--divider-color); + } + + .trigger-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .trigger-entity-name { font-weight: 500; font-size: 14px; } + .trigger-entity-id { font-size: 11px; color: var(--secondary-text-color); font-family: monospace; } + + .entity-link { + cursor: pointer; + text-decoration: underline dotted; + text-underline-offset: 2px; + } + .entity-link:hover { + color: var(--primary-color); + text-decoration: underline solid; + } + + .trigger-value-row { + display: flex; + align-items: baseline; + gap: 6px; + margin: 4px 0; + } + + .trigger-current { font-size: 28px; font-weight: 700; color: var(--primary-text-color); } + .trigger-current.active { color: var(--maint-triggered-color); } + .trigger-unit { font-size: 14px; color: var(--secondary-text-color); } + + /* Counter progress ("8,507 / 15,000 km · 57 %" + bar) */ + .counter-progress { margin: 6px 0 4px; } + .counter-progress-nums { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + } + .counter-progress-main { font-size: 26px; font-weight: 700; color: var(--primary-text-color); } + .counter-progress-target { font-size: 15px; font-weight: 500; color: var(--secondary-text-color); } + .counter-progress-pct { font-size: 15px; font-weight: 700; } + .counter-progress-pct.ok { color: var(--success-color, #4caf50); } + .counter-progress-pct.near { color: var(--warning-color, #ff9800); } + .counter-progress-pct.over { color: var(--error-color, #f44336); } + .counter-progress-bar { + height: 8px; border-radius: 4px; margin: 6px 0 4px; overflow: hidden; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.08)); + } + .counter-progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } + .counter-progress-fill.ok { background: var(--success-color, #4caf50); } + .counter-progress-fill.near { background: var(--warning-color, #ff9800); } + .counter-progress-fill.over { background: var(--error-color, #f44336); } + .counter-progress-caption { font-size: 12px; color: var(--secondary-text-color); } + + /* Note under a chart that fell back to sparse maintenance-event values */ + .chart-note { + display: flex; align-items: center; gap: 6px; margin-top: 2px; + font-size: 12px; color: var(--secondary-text-color); + } + .chart-note ha-icon { --mdc-icon-size: 15px; flex: none; } + + .trigger-limits { + display: flex; + gap: 16px; + font-size: 13px; + color: var(--secondary-text-color); + margin: 6px 0; + flex-wrap: wrap; + } + + .trigger-limit-item { + display: flex; + align-items: center; + gap: 4px; + } + + .trigger-limit-item .dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + .trigger-limit-item .dot.warn { background: var(--error-color, #f44336); } + .trigger-limit-item .dot.range { background: var(--secondary-text-color); } + .trigger-limit-item .dot.ok { background: var(--maint-ok-color); } + + /* Row action buttons */ + .row-actions { + display: flex; + gap: 0; + flex-shrink: 0; + margin-left: auto; + } + + .row-actions mwc-icon-button { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 18px; + } + + .row-actions .btn-complete { color: var(--maint-ok-color); } + .row-actions .btn-skip { color: var(--secondary-text-color); } + + /* Days bar for overview */ + .due-cell { + display: flex; + flex-direction: column; + align-items: flex-end; + min-width: 90px; + gap: 2px; + } + + .due-text { font-size: 13px; } + + .days-bar { + width: 100%; + height: 3px; + background: var(--divider-color); + border-radius: 2px; + overflow: hidden; + } + + .days-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + /* Trigger progress bar (overview rows). width:100% — the due-cell doesn't + stretch its children (align-items: flex-end), so without it the bar + shrinks to its label and reads shorter than the days-bar in other rows. */ + .trigger-progress { + display: flex; + flex-direction: column; + gap: 2px; + width: 100%; + min-width: 90px; + } + + .trigger-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .trigger-progress-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + .trigger-progress-label { + font-size: 12px; + color: var(--secondary-text-color); + text-align: right; + } + + /* Days progress bar (detail view) */ + .days-progress { + margin: 8px 0 16px; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .days-progress-labels { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .days-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .days-progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + .days-progress-text { + font-size: 13px; + font-weight: 500; + text-align: center; + margin-top: 6px; + color: var(--primary-text-color); + } + + /* Mini-sparkline in overview rows */ + .mini-sparkline { + width: 60px; + height: 20px; + display: block; + margin-top: 2px; + opacity: 0.7; + } + + /* Overflow indicator for overdue progress bars */ + .days-bar-fill.overflow, + .days-progress-fill.overflow, + .trigger-progress-fill.overflow { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 3px, + rgba(255,255,255,0.2) 3px, + rgba(255,255,255,0.2) 6px + ); + animation: overflow-pulse 2s ease-in-out infinite; + } + + @keyframes overflow-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + /* Budget KPI tiles in the stats strip (#125) — replaced the full-width + budget-bars row. The spent amount inherits .stat-value's full 24px bold + so the budget tiles read exactly like the other KPI chips (user report + 2026-08-24: the old 15px override made them visibly smaller); only the + "/ max" suffix stays secondary. */ + .stat-item.budget-tile .budget-tile-value { + white-space: nowrap; + } + /* The "/ max" ratio is its OWN small line between value and bar — inline + it overflowed the ~150px grid cell into the neighbouring tile once the + value took the full 24px. */ + .budget-tile-max { + font-size: 11px; + line-height: 1.2; + color: var(--secondary-text-color); + white-space: nowrap; + } + .budget-tile-bar { + width: 100%; + max-width: 130px; + height: 4px; + border-radius: 2px; + background: var(--divider-color); + overflow: hidden; + margin-top: 5px; + } + .budget-tile-bar > div { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + /* Groups section */ + .groups-section { + padding: 8px 16px 16px; + } + + .groups-section h3 { + font-size: 14px; + font-weight: 500; + color: var(--secondary-text-color); + margin: 0 0 8px; + } + + .groups-grid { + display: flex; + gap: 12px; + flex-wrap: wrap; + } + + .group-card { + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 12px 16px; + min-width: 180px; + flex: 1; + max-width: 300px; + cursor: default; + } + + .group-card-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + } + + .group-card-name { + font-weight: 500; + font-size: 14px; + margin-bottom: 4px; + } + + .group-card-actions { + display: flex; + gap: 0; + } + .group-card-actions mwc-icon-button { + --mdc-icon-button-size: 28px; + --mdc-icon-size: 16px; + color: var(--secondary-text-color); + } + + .groups-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + .groups-header h3 { margin: 0; } + + .seasonal-actions { + display: flex; + justify-content: flex-end; + padding: 4px 0; + } + + .group-card-desc { + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + + .group-card-tasks { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .group-task-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + } + + /* Adaptive scheduling suggestion badge */ + .suggestion-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 16px; + font-size: 12px; + font-weight: 500; + background: var(--info-color, #2196f3); + color: white; + margin-left: 8px; + } + + .suggestion-actions { + display: flex; + gap: 8px; + margin-top: 8px; + } + + .suggestion-actions ha-button { + --ha-button-font-size: 12px; + } + + .confidence-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .confidence-dot.low { background: var(--secondary-text-color); } + .confidence-dot.medium { background: var(--warning-color, #ff9800); } + .confidence-dot.high { background: var(--success-color, #4caf50); } + + /* (The complete-dialog's .feedback-* rules live in complete-dialog.ts — + this sheet carried a dead byte-identical copy until the 2026-08 drift + audit; complete-dialog never imports sharedStyles.) */ + + /* Seasonal chart */ + .seasonal-chart { + padding: 12px 16px; + margin: 8px 0; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .seasonal-chart-title { + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + } + + .seasonal-chart-title .source-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + font-weight: 400; + } + + .seasonal-chart svg { + width: 100%; + height: 100px; + display: block; + } + + .seasonal-labels { + display: flex; + justify-content: space-between; + padding: 0 2px; + margin-top: 4px; + } + + .seasonal-label { + font-size: 10px; + color: var(--secondary-text-color); + text-align: center; + flex: 1; + } + + .seasonal-label.active-month { + font-weight: 700; + color: var(--primary-color); + } + + .seasonal-factor-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + margin-left: 6px; + } + + .seasonal-factor-tag.short { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + .seasonal-factor-tag.long { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + + /* --- Sensor Prediction Section (Phase 3) --- */ + + .prediction-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .prediction-urgency-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-bottom: 12px; + border-radius: 8px; + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + font-size: 13px; + font-weight: 500; + } + .prediction-urgency-banner ha-svg-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + + .prediction-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .prediction-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .prediction-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + } + + .prediction-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + .prediction-item ha-svg-icon { + --mdc-icon-size: 14px; + color: var(--secondary-text-color); + flex-shrink: 0; + } + + .prediction-label { + font-weight: 500; + } + + .prediction-value { + font-weight: 600; + color: var(--primary-text-color); + } + .prediction-value.rising { color: var(--error-color, #f44336); } + .prediction-value.falling { color: var(--info-color, #2196f3); } + .prediction-value.stable { color: var(--success-color, #4caf50); } + .prediction-value.exceeded { color: var(--error-color, #f44336); font-weight: 700; } + .prediction-value.urgent { color: var(--warning-color, #ff9800); font-weight: 700; } + + .prediction-rate { + font-size: 11px; + opacity: 0.7; + font-family: monospace; + } + + .prediction-date { + font-size: 11px; + opacity: 0.7; + } + + .prediction-cycles { + font-size: 11px; + opacity: 0.7; + } + + .prediction-entity { + font-size: 10px; + opacity: 0.6; + font-family: monospace; + } + + /* --- Weibull Reliability Section (Phase 4) --- */ + + .weibull-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .weibull-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .weibull-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .weibull-chart svg { + width: 100%; + height: 160px; + display: block; + } + + .weibull-info-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 10px; + } + + .weibull-info-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + + .weibull-info-value { + font-weight: 600; + color: var(--primary-text-color); + } + + /* Beta interpretation badge */ + .beta-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + } + .beta-badge ha-svg-icon { + --mdc-icon-size: 14px; + } + + .beta-badge.early_failures { + background: rgba(244, 67, 54, 0.15); + color: var(--error-color, #f44336); + } + .beta-badge.random_failures { + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + } + .beta-badge.wear_out { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + .beta-badge.highly_predictable { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + /* Confidence interval range bar */ + .confidence-range { + margin-top: 12px; + } + + .confidence-range-title { + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .confidence-bar { + position: relative; + width: 100%; + height: 8px; + background: var(--divider-color, #e0e0e0); + border-radius: 4px; + overflow: visible; + } + + .confidence-fill { + position: absolute; + height: 100%; + border-radius: 4px; + background: var(--primary-color, #03a9f4); + opacity: 0.25; + } + + .confidence-marker { + position: absolute; + top: -4px; + width: 3px; + height: 16px; + border-radius: 1px; + transform: translateX(-50%); + } + .confidence-marker.recommended { + background: var(--success-color, #4caf50); + } + .confidence-marker.current { + background: var(--primary-color, #03a9f4); + } + + .confidence-labels { + display: flex; + justify-content: space-between; + margin-top: 4px; + } + + .confidence-text { + font-size: 10px; + color: var(--secondary-text-color); + } + .confidence-text.low { + text-align: left; + } + .confidence-text.high { + text-align: right; + } + + .task-disabled { opacity: 0.5; } + .badge-disabled { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--disabled-color, #9e9e9e); + color: white; + } + + /* ── Shared responsive styles (panel + card) ── */ + @media (max-width: 600px) { + .row-actions mwc-icon-button { + --mdc-icon-button-size: 44px; + --mdc-icon-size: 22px; + } + + .due-cell { min-width: 70px; } + + .trigger-card { padding: 10px 12px; } + .trigger-current { font-size: 22px; } + + .prediction-grid { flex-direction: column; gap: 8px; } + + .weibull-info-row { flex-direction: column; gap: 8px; } + + /* Budget tiles on narrow screens (#125): the spent amount keeps the + full chip size (consistency, user report 2026-08-24); the "/ max" + suffix is hidden instead — the bar and the title carry the ratio. */ + .budget-tile-max { display: none; } + + .group-card { min-width: 0; max-width: 100%; } + + .filter-chip { padding: 6px 12px; font-size: 13px; } + + .history-details { flex-wrap: wrap; gap: 6px; } + + .sparkline-container { max-width: 100%; overflow: hidden; } + .sparkline-svg { height: 100px; } + + .stats-bar { gap: 8px; padding: 12px; } + /* min-width: 0 (NOT a fixed floor) — a fixed min-width re-enables the + grid's auto minimum, so the 5 KPI tracks couldn't shrink below their + label text and the last KPI clipped off-screen on phones (the header + only *looked* cut — .content scrolls sideways, but nothing hints so). + With 0 the tracks compress and the labels wrap to a second line. */ + .stat-item { min-width: 0; } + .stat-item.clickable { padding: 4px 4px; } + .stat-item .stat-label { font-size: 11px; white-space: normal; text-align: center; line-height: 1.2; } + .stat-value { font-size: 20px; } + } +`;export{it as a,q as b,pt as c,_t as d,x as e,p as f,ht as g,$ as h,ne as i,Ct as j,Tt as k,Ae as l,Ot as m,Ke as n,Qe as o,co as p,u as q,po as r,_o as s,et as t,tt as u,Ce as v,uo as w,ho as x,Ee as y,Te as z,go as A,mo as B,fo as C,bo as D}; +/*! Bundled license information: + +@lit/reactive-element/css-tag.js: + (** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/reactive-element.js: +lit-html/lit-html.js: +lit-element/lit-element.js: +@lit/reactive-element/decorators/custom-element.js: +@lit/reactive-element/decorators/property.js: +@lit/reactive-element/decorators/state.js: +@lit/reactive-element/decorators/event-options.js: +@lit/reactive-element/decorators/base.js: +@lit/reactive-element/decorators/query.js: +@lit/reactive-element/decorators/query-all.js: +@lit/reactive-element/decorators/query-async.js: +@lit/reactive-element/decorators/query-assigned-nodes.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +lit-html/is-server.js: + (** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/query-assigned-elements.js: + (** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) +*/ diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VZ2E6UPO.js b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VZ2E6UPO.js new file mode 100644 index 00000000..46314cbc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/chunk-VZ2E6UPO.js @@ -0,0 +1,1317 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{a as T,d as k,e as q}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VFPFVFKO.js";import{a as M}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-J4OBOTUE.js";import{a as R}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-KKI2ZPWC.js";import{a,b as L,c as o,f as p,h as N,l as $,m as l,q as s,s as H,v as F,z as S}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";var C=["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"],O=["sensor"],U=["temperature","humidity","pressure"];var z=["cleaning","inspection","replacement","calibration","service","reading","custom"],V=["low","normal","high"],W=["time_based","weekdays","nth_weekday","day_of_month","sensor_based","one_time","manual"],A=["weekdays","nth_weekday","day_of_month"],D=["threshold","counter","state_change","runtime"],K=[...D,"compound"],y={alpha:"0.3",min:"7",max:"365"};function Y(){return{entityIds:"",type:"threshold",attribute:"",above:"",below:"",equals:"",notEquals:"",forMinutes:"0",targetValue:"",deltaMode:!1,fromState:"",toState:"",targetChanges:"",runtimeHours:"",onStates:"",carry:{}}}var J=new Set(["entity_id","entity_ids","type","attribute","trigger_above","trigger_below","trigger_equals","trigger_not_equals","trigger_for_minutes","trigger_target_value","trigger_delta_mode","trigger_from_state","trigger_to_state","trigger_target_changes","trigger_runtime_hours","trigger_on_states"]);function G(u){return{entityIds:(u.entity_ids||(u.entity_id?[u.entity_id]:[])).join(", "),type:u.type||"threshold",attribute:u.attribute||"",above:u.trigger_above?.toString()??"",below:u.trigger_below?.toString()??"",equals:u.trigger_equals?.toString()??"",notEquals:u.trigger_not_equals?.toString()??"",forMinutes:u.trigger_for_minutes?.toString()??"0",targetValue:u.trigger_target_value?.toString()??"",deltaMode:u.trigger_delta_mode||!1,fromState:u.trigger_from_state||"",toState:u.trigger_to_state||"",targetChanges:u.trigger_target_changes?.toString()??"",runtimeHours:u.trigger_runtime_hours?.toString()??"",onStates:(u.trigger_on_states||[]).join(", "),carry:Object.fromEntries(Object.entries(u).filter(([e])=>!J.has(e)&&!e.startsWith("_")))}}function Q(u){let b=u.entityIds.split(",").map(i=>i.trim()).filter(Boolean);if(b.length===0)return null;let e={...u.carry||{},entity_id:b[0],entity_ids:b,type:u.type};if(u.attribute&&(e.attribute=u.attribute),u.type==="threshold"){let i=parseFloat(u.above);isNaN(i)||(e.trigger_above=i);let t=parseFloat(u.below);isNaN(t)||(e.trigger_below=t);let r=parseFloat(u.equals);isNaN(r)||(e.trigger_equals=r);let h=parseFloat(u.notEquals);isNaN(h)||(e.trigger_not_equals=h);let _=parseInt(u.forMinutes,10);isNaN(_)||(e.trigger_for_minutes=_)}else if(u.type==="counter"){let i=parseFloat(u.targetValue);isNaN(i)||(e.trigger_target_value=i),e.trigger_delta_mode=u.deltaMode}else if(u.type==="state_change"){u.fromState&&(e.trigger_from_state=u.fromState),u.toState&&(e.trigger_to_state=u.toState);let i=parseInt(u.targetChanges,10);isNaN(i)||(e.trigger_target_changes=i)}else if(u.type==="runtime"){let i=parseFloat(u.runtimeHours);isNaN(i)||(e.trigger_runtime_hours=i);let t=(u.onStates||"").split(",").map(r=>r.trim()).filter(Boolean);t.length>0&&(e.trigger_on_states=t)}return e}function X(u){return Array.from({length:7},(b,e)=>S(e,u,"short"))}function Z(u){let b=new Intl.DateTimeFormat(u||"en",{month:"short"});return Array.from({length:12},(e,i)=>b.format(new Date(2021,i,1)))}var n=class n extends N{constructor(){super(...arguments);this.checklistsEnabled=!1;this.scheduleTimeEnabled=!1;this.completionActionsEnabled=!1;this.defaultWarningDays=7;this.parts=[];this._foreignOwners=[];this._open=!1;this._entityPickerFallback=!1;this._pickerProbeStrikes=0;this._loading=!1;this._error="";this._entryId="";this._taskId=null;this._objectChoices=[];this._name="";this._type="custom";this._scheduleType="time_based";this._intervalDays="30";this._intervalUnit="days";this._dueDate="";this._warningDays="7";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._schedulePreview=[];this._schedulePreviewEnded=!1;this._previewSeq=0;this._notes="";this._documentationUrl="";this._customIcon="";this._priority="normal";this._labels="";this._enabled=!0;this._triggerEntityId="";this._triggerEntityIds=[];this._triggerEntityLogic="any";this._triggerAttribute="";this._triggerType="threshold";this._triggerAbove="";this._triggerBelow="";this._triggerEquals="";this._triggerNotEquals="";this._triggerForMinutes="0";this._triggerCombinator="any";this._triggerTargetValue="";this._triggerDeltaMode=!1;this._triggerBaselineValue="";this._liveBaselineValue=null;this._autoCompleteOnRecovery=!1;this._triggerFromState="";this._triggerToState="";this._triggerTargetChanges="";this._triggerRuntimeHours="";this._triggerOnStates="";this._compoundLogic="AND";this._compoundConditions=[];this._suggestedAttributes=[];this._availableAttributes=[];this._entityDomain="";this._lastPerformed="";this._nfcTagId="";this._requireTagScan=!1;this._readingUnit="";this._consumesParts={};this._partsLoadFailed=!1;this._availableTags=[];this._responsibleUserId=null;this._assigneePool=[];this._rotationStrategy="";this._availableUsers=[];this._checklistText="";this._phaseDefs=[];this._phaseSeq=[];this._requiredCompletion=[];this._scheduleTime="";this._actionService="";this._actionTargetEntity="";this._actionData={};this._actionDataJsonFallback="";this._actionTesting=!1;this._actionTestResult="";this._actionTestError="";this._qcNotes="";this._qcCost="";this._qcDuration="";this._qcFeedback="";this._environmentalEntity="";this._environmentalAttribute="";this._environmentalInitial="";this._environmentalAttributeInitial="";this._adaptiveEnabled=!1;this._adaptiveAlpha=y.alpha;this._adaptiveMin=y.min;this._adaptiveMax=y.max;this._adaptiveSeasonal=!0;this._adaptivePrediction=!0;this._adaptiveInitial="";this._userService=null;this._conditionAttrOptions={};this._conditionAttrPending=new Set}_adaptiveSnapshot(){return JSON.stringify([this._adaptiveEnabled,this._adaptiveAlpha,this._adaptiveMin,this._adaptiveMax,this._adaptiveSeasonal,this._adaptivePrediction])}get _lang(){return H(this.hass)}async openCreate(e,i){this._entryId=e,this._taskId=null,this._error="",!e&&i&&i.length>0?(this._objectChoices=i.map(t=>({entry_id:t.entry_id,name:t.object.name})).sort((t,r)=>t.name.localeCompare(r.name)),this._entryId=this._objectChoices[0].entry_id):this._objectChoices=[],this._resetFields(),await Promise.all([this._loadUsers(),this._loadTags(),this._loadParts(),this._loadForeignPools()]),this._open=!0}async openEdit(e,i){this._entryId=e,this._taskId=i.id,this._error="",this._objectChoices=[],this._name=i.name,this._type=i.type,this._scheduleType=i.schedule_type,this._intervalDays=i.interval_days!=null?String(i.interval_days):"",this._intervalUnit=i.interval_unit||"days",this._dueDate=i.due_date||"";let t=i.schedule;this._weekdays=t?.kind==="weekdays"?[...t.weekdays??[]]:[],this._nth=t?.kind==="nth_weekday"?String(t.nth??1):"1",this._nthWeekday=t?.kind==="nth_weekday"?String(t.weekday??5):"5",this._domDay=t?.kind==="day_of_month"&&(t.day??1)>=1?String(t.day??1):"1",this._domLastDay=t?.kind==="day_of_month"&&t.day===-1,this._domBusiness=t?.kind==="day_of_month"&&t.business===!0,this._calOffset=t?.offset?String(t.offset):"0",this._seasonMonths=Array.isArray(t?.season_months)?[...t.season_months]:[];let r=t?.ends;r&&typeof r.count=="number"?(this._endsMode="count",this._endsCount=String(r.count),this._endsUntil=""):r&&typeof r.until=="string"?(this._endsMode="until",this._endsUntil=r.until,this._endsCount=""):(this._endsMode="never",this._endsCount="",this._endsUntil=""),this._warningDays=i.warning_days.toString(),this._earliestCompletionDays=i.earliest_completion_days!=null?String(i.earliest_completion_days):"",this._intervalAnchor=i.interval_anchor||"completion",this._notes=i.notes||"",this._documentationUrl=i.documentation_url||"",this._customIcon=i.custom_icon||"",this._priority=i.priority||"normal",this._labels=(i.labels||[]).join(", "),this._enabled=i.enabled!==!1,this._lastPerformed=i.last_performed||"",this._nfcTagId=i.nfc_tag_id||"",this._requireTagScan=!!i.require_tag_scan,this._readingUnit=i.reading_unit||"",this._consumesParts=Object.fromEntries((i.consumes_parts||[]).map(d=>[T(d),{...d}])),this._responsibleUserId=i.responsible_user_id||null,this._assigneePool=[...i.assignee_pool||[]],this._rotationStrategy=i.rotation_strategy||"",this._checklistText=(i.checklist||[]).join(` +`),this._phaseDefs=Object.entries(i.phases||{}).map(([d,g])=>{let{name:v,checklist:m,consumes_parts:f,required_completion_fields:ee,...j}=g,x=g.consumes_parts||[],I=x.findIndex(E=>!E.entry_id),w=I>=0?x[I]:void 0;return{id:d,name:g.name||d,checklistText:(g.checklist||[]).join(` +`),partId:w?.part_id||"",partQty:w?.quantity!=null?String(w.quantity):"",reqOverride:g.required_completion_fields!==void 0,reqFields:[...g.required_completion_fields||[]],extraParts:x.filter((E,B)=>B!==I).map(E=>({...E})),carry:j}}),this._phaseSeq=[...i.phase_sequence||[]],this._requiredCompletion=[...i.required_completion_fields||[]],this._scheduleTime=i.schedule_time||"";let h=i.on_complete_action;if(h&&h.service){this._actionService=h.service;let d=h.target?.entity_id;this._actionTargetEntity=Array.isArray(d)?d[0]||"":d||"",this._actionData=h.data&&typeof h.data=="object"?{...h.data}:{},this._actionDataJsonFallback=""}else this._actionService="",this._actionTargetEntity="",this._actionData={},this._actionDataJsonFallback="";let _=i.quick_complete_defaults;this._qcNotes=_?.notes||"",this._qcCost=_?.cost!=null?String(_.cost):"",this._qcDuration=_?.duration!=null?String(_.duration):"",this._qcFeedback=_?.feedback||"";let c=i.adaptive_config||{};if(this._environmentalEntity=c.environmental_entity||"",this._environmentalAttribute=c.environmental_attribute||"",this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute,this._adaptiveEnabled=!!c.enabled,this._adaptiveAlpha=c.ewa_alpha?.toString()??y.alpha,this._adaptiveMin=c.min_interval_days?.toString()??y.min,this._adaptiveMax=c.max_interval_days?.toString()??y.max,this._adaptiveSeasonal=c.seasonal_enabled!==!1,this._adaptivePrediction=c.sensor_prediction_enabled!==!1,this._adaptiveInitial=this._adaptiveSnapshot(),i.trigger_config){let d=i.trigger_config;this._triggerEntityId=d.entity_id||d.entity_ids&&d.entity_ids[0]||"",this._triggerEntityIds=d.entity_ids||(d.entity_id?[d.entity_id]:[]),this._triggerEntityLogic=d.entity_logic||"any",this._triggerAttribute=d.attribute||"",this._triggerType=d.type||"threshold",this._triggerAbove=d.trigger_above?.toString()||"",this._triggerBelow=d.trigger_below?.toString()||"",this._triggerEquals=d.trigger_equals?.toString()||"",this._triggerNotEquals=d.trigger_not_equals?.toString()||"",this._triggerForMinutes=d.trigger_for_minutes?.toString()||"0",this._triggerCombinator=d.trigger_combinator==="all"?"all":"any",this._triggerTargetValue=d.trigger_target_value?.toString()||"",this._triggerDeltaMode=d.trigger_delta_mode||!1,this._triggerBaselineValue=d.trigger_baseline_value?.toString()||"",this._liveBaselineValue=i.trigger_baseline_value??null,this._autoCompleteOnRecovery=d.auto_complete_on_recovery||!1,this._triggerFromState=d.trigger_from_state||"",this._triggerToState=d.trigger_to_state||"",this._triggerTargetChanges=d.trigger_target_changes?.toString()||"",this._triggerRuntimeHours=d.trigger_runtime_hours?.toString()||"",this._triggerOnStates=(d.trigger_on_states||[]).join(", "),d.type==="compound"?(this._compoundLogic=d.compound_logic==="OR"?"OR":"AND",this._compoundConditions=(d.conditions||[]).map(G)):(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=y.alpha,this._adaptiveMin=y.min,this._adaptiveMax=y.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="",this._triggerDeltaMode=!1,this._triggerBaselineValue="",this._liveBaselineValue=null,this._autoCompleteOnRecovery=!1,this._triggerFromState="",this._triggerToState="",this._triggerTargetChanges="",this._triggerRuntimeHours="",this._triggerOnStates="",this._compoundLogic="AND",this._compoundConditions=[]}async _loadUsers(){this._userService||(this._userService=new M(this.hass));try{this._availableUsers=await this._userService.getUsers()}catch(e){console.error("Failed to load users:",e),this._availableUsers=[]}}_toggleAssignee(e){this._assigneePool=this._assigneePool.includes(e)?this._assigneePool.filter(i=>i!==e):[...this._assigneePool,e]}async _testAction(){let e=this._actionService.trim();if(!e||!/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(e)){this._actionTestResult="error",this._actionTestError="Invalid service format (expected 'domain.service')",setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},5e3);return}let[i,t]=e.split(".");if(!this.hass?.services?.[i]?.[t]){this._actionTestResult="error",this._actionTestError=`Service "${e}" is not registered in Home Assistant. Check spelling and that the integration providing it is loaded.`,setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},8e3);return}let r=this._actionTargetEntity.trim();if(r){let h=r.split(".")[0];if(h!==i&&!new Set(["homeassistant","scene","notify","persistent_notification"]).has(i)){this._actionTestResult="error",this._actionTestError=`Service "${e}" only works on ${i}.* entities; entity "${r}" is in ${h}.* \u2014 pick a service that matches the entity domain (e.g. ${h}.${t})`,setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},8e3);return}if(!this.hass.states?.[r]){this._actionTestResult="error",this._actionTestError=`Target entity "${r}" not found in Home Assistant \u2014 the entity may have been renamed or its integration removed.`,setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},8e3);return}}this._actionTestResult="ok",setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},5e3)}_buildActionData(){if(this._actionDataJsonFallback.trim())try{let e=JSON.parse(this._actionDataJsonFallback);if(e&&typeof e=="object"&&!Array.isArray(e))return e}catch{}return{...this._actionData}}_serviceSchema(){let e=this._actionService.trim();if(!e||!/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(e))return null;let[i,t]=e.split("."),r=this.hass?.services?.[i]?.[t]?.fields;return!r||Object.keys(r).length===0?null:Object.entries(r).map(([h,_])=>({name:h,required:!!_.required,selector:_.selector||{text:{}}}))}_renderCompletionActionsSection(e){if(!this.completionActionsEnabled)return p;let i=this._serviceSchema();return o` +
+ ${s("on_complete_action_title",e)} +

${s("on_complete_action_desc",e)}

+ {this._actionService=t.detail.value||"";let r=this._serviceSchema();if(r){let h=new Set(r.map(_=>_.name));this._actionData=Object.fromEntries(Object.entries(this._actionData).filter(([_])=>h.has(_)))}}} + > + s("on_complete_action_target",e)} + @value-changed=${t=>{let r=t.detail.value;this._actionTargetEntity=r.target_entity||""}} + > +

+ ${s("on_complete_action_target_hint",e)} +

+ ${i?o` + {this._actionData={...t.detail.value}}} + > + `:o` + {this._actionDataJsonFallback=t.target.value}} + > + `} +
+ + ${this._actionTestResult==="ok"?o`${s("on_complete_action_test_success",e)}`:p} + ${this._actionTestResult==="error"?o`
+ ${s("on_complete_action_test_failed",e)} + ${this._actionTestError?o`
${this._actionTestError}
`:p} +
`:p} +
+
+ +
+ ${s("quick_complete_defaults_title",e)} +

${s("quick_complete_defaults_desc",e)}

+ {this._qcNotes=t.target.value}} + > + {this._qcCost=t.target.value}} + > + {this._qcDuration=t.target.value}} + > + +
+ `}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(i=>i.entry_id!==this._entryId&&(i.parts||[]).length>0).map(i=>({entry_id:i.entry_id,name:i.object?.name||i.entry_id,parts:i.parts||[]})).sort((i,t)=>i.name.localeCompare(t.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(i=>{let t=i;this._conditionAttrOptions={...this._conditionAttrOptions,[e]:{suggested:t.suggested_attributes||[],available:t.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 i=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/entity/attributes",entity_id:e});this._entityDomain=i.domain||"",this._suggestedAttributes=i.suggested_attributes||[],this._availableAttributes=i.available_attributes||[]}catch{this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain=""}}get _hasForeignPick(){return Object.values(this._consumesParts).some(e=>!!e.entry_id)}_renderConsumesRow(e,i){let t=T({part_id:e.id,entry_id:i}),r=this._consumesParts[t],h=i?{part_id:e.id,quantity:1,entry_id:i}:{part_id:e.id,quantity:1};return o` +
+ + ${r!==void 0?o`{let c=parseFloat(_.target.value);this._consumesParts={...this._consumesParts,[t]:{...h,quantity:Number.isFinite(c)&&c>=.01?c:1}}}} + />`:p} +
+ `}_toggleRequired(e,i){let t=new Set(this._requiredCompletion);i?t.add(e):t.delete(e),this._requiredCompletion=[...t]}_phaseSlug(e){let i=e.toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,24)||"phase",t=i,r=2;for(;this._phaseDefs.some(h=>h.id===t);)t=`${i}-${r++}`;return t}_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(i=>i.id!==e),this._phaseSeq=this._phaseSeq.filter(i=>i!==e)}_patchPhaseDef(e,i){this._phaseDefs=this._phaseDefs.map(t=>t.id===e?{...t,...i}:t)}_renderPhasesEditor(e){let i=t=>this._phaseDefs.find(r=>r.id===t)?.name||t;return o` +

${s("phases_section",e)}

+
${s("phases_hint",e)}
+ ${this._phaseDefs.map(t=>o` +
+
+ this._patchPhaseDef(t.id,{name:r.target.value})} + > + ${this.parts.length?o` + + ${t.partId?o` + this._patchPhaseDef(t.id,{partQty:r.target.value})} /> + `:p} + `:p} + this._removePhaseDef(t.id)}> + + +
+ ${this.checklistsEnabled?o` + + `:p} + + ${t.reqOverride?o` +
+ ${k.map(r=>o` + + `)} +
+ `:p} +
+ `)} + + ${s("phase_add",e)} + + ${this._phaseDefs.some(t=>t.name.trim())?o` +
${s("phase_sequence_label",e)}
+
+ ${this._phaseSeq.map((t,r)=>o` + + ${r+1}. ${i(t)} + + + `)} + +
+ `:p} + `}async _save(){if(!this._loading&&this._name.trim()){if(this._adaptiveSnapshot()!==this._adaptiveInitial){let e=parseInt(this._adaptiveMin,10),i=parseInt(this._adaptiveMax,10);if(!isNaN(e)&&!isNaN(i)&&e>i){this._error=`${s("adaptive_min_interval",this._lang)} > ${s("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))},i=this._earliestCompletionDays.trim();e.earliest_completion_days=i===""?null:Math.max(0,parseInt(i,10)||0),this._taskId&&(e.task_id=this._taskId),this._scheduleType==="one_time"?(e.due_date=this._dueDate||null,e.interval_days=null):A.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(_=>_.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 _={};for(let d of this._phaseDefs){if(!d.name.trim())continue;let g={...d.carry,name:d.name.trim()},v=d.checklistText.split(` +`).map(f=>f.trim()).filter(Boolean);v.length&&(g.checklist=v);let m=[];if(d.partId){let f=parseFloat(d.partQty);m.push({part_id:d.partId,quantity:Number.isFinite(f)&&f>0?f:1})}for(let f of d.extraParts)m.push(f.entry_id?{part_id:f.part_id,quantity:f.quantity,entry_id:f.entry_id}:{part_id:f.part_id,quantity:f.quantity});m.length&&(g.consumes_parts=m),d.reqOverride&&(g.required_completion_fields=[...d.reqFields]),_[d.id]=g}let c=this._phaseSeq.filter(d=>d in _);e.phases=Object.keys(_).length&&c.length?_:null,e.phase_sequence=e.phases?c:null}if((this.parts.length||this._foreignOwners.length)&&(e.consumes_parts=Object.values(this._consumesParts).map(_=>_.entry_id?{part_id:_.part_id,quantity:_.quantity,entry_id:_.entry_id}:{part_id:_.part_id,quantity:_.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 _=this._compoundConditions.map(Q).filter(c=>c!==null);if(_.length>0){let c={type:"compound",compound_logic:this._compoundLogic,conditions:_};this._autoCompleteOnRecovery&&(c.auto_complete_on_recovery=!0),this._triggerCombinator==="all"&&(c.trigger_combinator="all"),e.trigger_config=c}else this._taskId&&(e.trigger_config=null)}else if(this._scheduleType==="sensor_based"&&this._triggerEntityId){let _=this._triggerEntityIds.length>0?this._triggerEntityIds:[this._triggerEntityId],c={entity_id:_[0],entity_ids:_,type:this._triggerType};if(this._triggerAttribute&&(c.attribute=this._triggerAttribute),this._autoCompleteOnRecovery&&(c.auto_complete_on_recovery=!0),this._triggerCombinator==="all"&&(c.trigger_combinator="all"),_.length>1&&(c.entity_logic=this._triggerEntityLogic),this._triggerType==="threshold"){if(this._triggerAbove){let d=parseFloat(this._triggerAbove);isNaN(d)||(c.trigger_above=d)}if(this._triggerBelow){let d=parseFloat(this._triggerBelow);isNaN(d)||(c.trigger_below=d)}if(this._triggerEquals){let d=parseFloat(this._triggerEquals);isNaN(d)||(c.trigger_equals=d)}if(this._triggerNotEquals){let d=parseFloat(this._triggerNotEquals);isNaN(d)||(c.trigger_not_equals=d)}if(this._triggerForMinutes){let d=parseInt(this._triggerForMinutes,10);isNaN(d)||(c.trigger_for_minutes=d)}}else if(this._triggerType==="counter"){if(this._triggerTargetValue){let d=parseFloat(this._triggerTargetValue);isNaN(d)||(c.trigger_target_value=d)}if(c.trigger_delta_mode=this._triggerDeltaMode,this._triggerDeltaMode&&this._triggerBaselineValue){let d=parseFloat(this._triggerBaselineValue);!isNaN(d)&&d>=0&&(c.trigger_baseline_value=d)}}else if(this._triggerType==="state_change"){if(this._triggerFromState&&(c.trigger_from_state=this._triggerFromState),this._triggerToState&&(c.trigger_to_state=this._triggerToState),this._triggerTargetChanges){let d=parseInt(this._triggerTargetChanges,10);isNaN(d)||(c.trigger_target_changes=d)}if(this._triggerForMinutes){let d=parseInt(this._triggerForMinutes,10);isNaN(d)||(c.trigger_for_minutes=d)}}else if(this._triggerType==="runtime"){if(this._triggerRuntimeHours){let g=parseFloat(this._triggerRuntimeHours);isNaN(g)||(c.trigger_runtime_hours=g)}let d=this._triggerOnStates.split(",").map(g=>g.trim()).filter(Boolean);d.length>0&&(c.trigger_on_states=d)}e.trigger_config=c}else this._taskId&&(e.trigger_config=null);if(this.scheduleTimeEnabled&&this._scheduleType==="time_based"){let _=this._scheduleTime.trim();e.schedule_time=/^([01]\d|2[0-3]):[0-5]\d$/.test(_)?_:null}if(this.checklistsEnabled){let _=this._checklistText.split(` +`).map(c=>c.trim()).filter(Boolean).slice(0,100);e.checklist=_.length?_:null}if(this.completionActionsEnabled){let _=this._actionService.trim();if(_&&/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(_)){let v={service:_},m=this._actionTargetEntity.trim();m&&(v.target={entity_id:m});let f=this._buildActionData();Object.keys(f).length>0&&(v.data=f),e.on_complete_action=v}else e.on_complete_action=null;let c={};this._qcNotes.trim()&&(c.notes=this._qcNotes.trim());let d=parseFloat(this._qcCost);!isNaN(d)&&d>=0&&(c.cost=d);let g=parseInt(this._qcDuration,10);!isNaN(g)&&g>=0&&(c.duration=g),this._qcFeedback&&(c.feedback=this._qcFeedback),e.quick_complete_defaults=Object.keys(c).length?c:null}let t=await this.hass.connection.sendMessagePromise(e),r=this._taskId||t?.task_id,h=this._environmentalEntity!==this._environmentalInitial||this._environmentalAttribute!==this._environmentalAttributeInitial;if(r&&this._scheduleType==="sensor_based"&&h)try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/set_environmental_entity",entry_id:this._entryId,task_id:r,environmental_entity:this._environmentalEntity||null,environmental_attribute:this._environmentalAttribute||null}),this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute}catch{}if(r&&this._adaptiveSnapshot()!==this._adaptiveInitial){let _=parseFloat(this._adaptiveAlpha),c=parseInt(this._adaptiveMin,10),d=parseInt(this._adaptiveMax,10);try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/set_adaptive",entry_id:this._entryId,task_id:r,enabled:this._adaptiveEnabled,..._>=.1&&_<=.9?{ewa_alpha:_}:{},...!isNaN(c)&&c>=1?{min_interval_days:c}:{},...!isNaN(d)&&d>=1?{max_interval_days:d}:{},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=R(e,this._lang,s("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 p;let e=this._lang,i=this._triggerType==="compound";return o` +

${s("trigger_configuration",e)}

+
+ + +
+ ${i?this._renderCompoundEditor():o` + ${this._entityPickerFallback?o` + 0?this._triggerEntityIds.join(", "):this._triggerEntityId} + @input=${t=>{let h=t.target.value.split(",").map(_=>_.trim()).filter(Boolean);this._triggerEntityId=h[0]||"",this._triggerEntityIds=h,h[0]&&this._fetchEntityAttributes(h[0])}} + > + `:o` + 0?this._triggerEntityIds:this._triggerEntityId?[this._triggerEntityId]:[]}} + .computeLabel=${()=>s("entity_id",e)} + @value-changed=${t=>{let r=(t.detail.value.trigger_entities||[]).filter(Boolean);this._triggerEntityId=r[0]||"",this._triggerEntityIds=r,r[0]?this._fetchEntityAttributes(r[0]):this._fetchEntityAttributes("")}} + >`} + ${this._triggerEntityIds.length>1?o` +
+ + +
+ `:p} + ${this._renderAttributeSelect({label:s("attribute_optional",e),value:this._triggerAttribute,suggested:this._suggestedAttributes,available:this._availableAttributes,onSelect:t=>this._triggerAttribute=t})} + ${this._renderTriggerTypeFields()} + ${this._renderTriggerLiveHint()} + `} + +
${s("auto_complete_on_recovery_help",e)}
+ this._intervalDays=t.target.value} + > + ${this._intervalDays?this._renderUnitSelect():p} + ${this._intervalDays?o` +
+ + +
+ `:p} + `}_patchCondition(e,i){this._compoundConditions=this._compoundConditions.map((t,r)=>r===e?{...t,...i}:t)}_addCondition(){this._compoundConditions=[...this._compoundConditions,Y()]}_removeCondition(e){this._compoundConditions=this._compoundConditions.filter((i,t)=>t!==e)}_renderCompoundEditor(){let e=this._lang;return o` +
+ + +
+
${s("compound_help",e)}
+ ${this._compoundConditions.length===0?o`
${s("compound_no_conditions",e)}
`:this._compoundConditions.map((i,t)=>this._renderCondition(i,t))} + + `}_renderCondition(e,i){let t=this._lang,r=i+1;return o` +
+
+ ${s("compound_condition",t)} ${r} + +
+ ${this._entityPickerFallback?o` + this._patchCondition(i,{entityIds:h.target.value})} + > + `:o` + h.trim()).filter(Boolean)}} + .computeLabel=${()=>s("entity_id",t)} + @value-changed=${h=>{let _=(h.detail.value.condition_entities||[]).filter(Boolean);this._patchCondition(i,{entityIds:_.join(", ")})}} + >`} + ${this._renderConditionAttribute(e,i)} +
+ + +
+ ${this._renderConditionTypeFields(e,i)} +
+ `}_renderStateField(e){return this._entityPickerFallback||!e.entityId?o` + e.onInput(i.target.value)} + > + `:o` + e.label} + @value-changed=${i=>e.onInput((i.detail.value.s||"").trim())} + > + `}_renderOnStatesField(e){let i=this._lang;return this._entityPickerFallback||!e.entityId?o` + e.onInput(t.target.value)} + > + `:o` + t.trim()).filter(Boolean)}} + .computeLabel=${()=>s("runtime_on_states",i)} + @value-changed=${t=>e.onInput((t.detail.value.s||[]).join(", "))} + > + `}_renderAdaptiveSection(e){return this._scheduleType==="one_time"||this._scheduleType==="manual"?p:o` +
+ ${s("adaptive_section_title",e)} + + ${this._adaptiveEnabled?o` + this._adaptiveMin=i.target.value} + > + this._adaptiveMax=i.target.value} + > + this._adaptiveAlpha=i.target.value} + > + + + `:p} +
+ `}_renderAttributeSelect(e){let i=this._lang;return e.available.length>0?o` +
+ + +
+ `:o` + e.onSelect(t.target.value.trim())} + > + `}_renderEnvironmentalAttribute(e){this._fetchConditionAttributes(this._environmentalEntity);let i=this._conditionAttrOptions[this._environmentalEntity];return this._renderAttributeSelect({label:s("environmental_attribute_optional",e),value:this._environmentalAttribute,suggested:i?.suggested??[],available:i?.available??[],onSelect:t=>this._environmentalAttribute=t})}_renderConditionAttribute(e,i){let t=e.entityIds.split(",")[0]?.trim()||"";t&&this._fetchConditionAttributes(t);let r=t?this._conditionAttrOptions[t]:void 0;return this._renderAttributeSelect({label:s("attribute_optional",this._lang),value:e.attribute,suggested:r?.suggested??[],available:r?.available??[],onSelect:h=>this._patchCondition(i,{attribute:h})})}_renderConditionTypeFields(e,i){let t=this._lang;if(e.type==="threshold")return o` + this._patchCondition(i,{above:r.target.value})}> + this._patchCondition(i,{below:r.target.value})}> + this._patchCondition(i,{equals:r.target.value})}> + this._patchCondition(i,{notEquals:r.target.value})}> + this._patchCondition(i,{forMinutes:r.target.value})}> + `;if(e.type==="counter")return o` + this._patchCondition(i,{targetValue:r.target.value})}> + + `;if(e.type==="state_change"){let r=e.entityIds.split(",")[0]?.trim()||"";return o` + ${this._renderStateField({label:s("from_state_optional",t),value:e.fromState,entityId:r,onInput:h=>this._patchCondition(i,{fromState:h})})} + ${this._renderStateField({label:s("to_state_optional",t),value:e.toState,entityId:r,onInput:h=>this._patchCondition(i,{toState:h})})} + this._patchCondition(i,{targetChanges:h.target.value})}> + `}if(e.type==="runtime"){let r=e.entityIds.split(",")[0]?.trim()||"";return o` + this._patchCondition(i,{runtimeHours:h.target.value})}> + ${this._renderOnStatesField({value:e.onStates,entityId:r,onInput:h=>this._patchCondition(i,{onStates:h})})} + `}return p}_renderUnitSelect(){let e=this._lang;return o` +
+ + +
`}_toggleWeekday(e){this._weekdays=this._weekdays.includes(e)?this._weekdays.filter(i=>i!==e):[...this._weekdays,e]}_previewScheduleDict(){if(this._scheduleType==="one_time")return this._dueDate?{kind:"one_time",due_date:this._dueDate}:null;if(A.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 i of e.keys())if(n._PREVIEW_RELEVANT.has(String(i))){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"),i=(this.shadowRoot?.querySelector(".content")?.offsetHeight??0)>0;if(!e||!i){this._pickerProbeStrikes=0;return}let t=(c,d,g=0)=>{if(!(!c||g>10)){(c.tagName?.toLowerCase()??"")==="ha-entity-picker"&&d.push(c);for(let v of[c.shadowRoot,c])if(v)for(let m of Array.from(v.children??[]))t(m,d,g+1)}},r=[...this.shadowRoot?.querySelectorAll("ha-form.entity-picker-form")??[]],h=[];for(let c of r)t(c,h);let _=h.length===0||h.some(c=>c.offsetHeight===0);if(e.offsetHeight===0||_){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 i=++this._previewSeq;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/schedule/preview",schedule:e,...this._lastPerformed?{last_performed:this._lastPerformed}:{}});if(i!==this._previewSeq)return;this._schedulePreview=t.occurrences||[],this._schedulePreviewEnded=!!t.series_ended}catch{}}_renderSchedulePreview(){if(this._schedulePreview.length===0)return p;let e=this._lang,i=this.scheduleTimeEnabled&&this._scheduleTime?` ${this._scheduleTime}`:"",t=this._schedulePreview.map((h,_)=>{let c=new Date(`${h}T12:00:00`).getDay();return`${S(c===0?6:c-1,e,"short")} ${F(h,e)}${_===0?i:""}`}).join(" \xB7 "),r=this._scheduleType==="time_based"&&this._intervalAnchor==="completion"?o`
${s("schedule_preview_ontime",e)}
`:p;return o` +
+ ${s("schedule_preview_title",e)}: ${t}${this._schedulePreviewEnded?o` ${s("schedule_preview_ends",e)}`:p} + ${r} +
+ `}_buildSchedule(){let e=t=>{let r=parseInt(this._calOffset,10)||0;return r&&(t.offset=Math.max(-15,Math.min(r,15))),t};if(this._scheduleType==="weekdays")return e({kind:"weekdays",weekdays:[...this._weekdays].sort((t,r)=>t-r)});if(this._scheduleType==="nth_weekday")return e({kind:"nth_weekday",nth:parseInt(this._nth,10),weekday:parseInt(this._nthWeekday,10)});let i={kind:"day_of_month",day:this._domLastDay?-1:parseInt(this._domDay,10)||1};return this._domBusiness&&(i.business=!0),e(i)}_recurrenceExtras(){let e={};if(this._seasonMonths.length&&(e.season_months=[...this._seasonMonths].sort((i,t)=>i-t)),this._endsMode==="count"){let i=parseInt(this._endsCount,10);i>=1&&(e.ends={count:i})}else this._endsMode==="until"&&this._endsUntil&&(e.ends={until:this._endsUntil});return e}_toggleSeasonMonth(e){this._seasonMonths=this._seasonMonths.includes(e)?this._seasonMonths.filter(i=>i!==e):[...this._seasonMonths,e]}_renderRecurrenceExtras(){let e=this._lang;if(!(this._scheduleType==="time_based"||A.includes(this._scheduleType)))return p;let t=Z(e);return o` + +
${s("season_window_hint",e)}
+
+ ${t.map((r,h)=>o` + `)} +
+ + +
+ +
+ ${this._endsMode==="count"?o` + this._endsCount=r.target.value} + >`:p} + ${this._endsMode==="until"?o` + this._endsUntil=r.target.value} + >`:p} + `}_renderCalendarFields(){let e=this._lang,i=X(e);if(this._scheduleType==="weekdays")return o` + +
+ ${i.map((t,r)=>o` + `)} +
+ ${this._renderCalOffsetField()}`;if(this._scheduleType==="nth_weekday"){let t=[["1",s("ord_1",e)],["2",s("ord_2",e)],["3",s("ord_3",e)],["4",s("ord_4",e)],["5",s("ord_5",e)],["-1",s("ord_last",e)]];return o` +
+ + +
+
+ + +
+ ${this._renderCalOffsetField()}`}return this._scheduleType==="day_of_month"?o` + ${this._domLastDay?p:o` + this._domDay=t.target.value} + >`} + + + ${this._renderCalOffsetField()}`:p}_renderCalOffsetField(){let e=this._lang;return o` + this._calOffset=i.target.value} + >`}_renderTriggerLiveHint(){if(this._triggerType==="compound")return p;let e=this._triggerEntityId||this._triggerEntityIds[0];if(!e||!this.hass?.states)return p;let i=this.hass.states[e];if(!i)return p;let t=this._lang,r=i.attributes?.unit_of_measurement,h=typeof r=="string"&&r?` ${r}`:"",_=this._triggerAttribute?i.attributes?.[this._triggerAttribute]:i.state,c=typeof _=="number"?_:parseFloat(String(_)),d=_!=="unknown"&&_!=="unavailable"&&_!=null&&!isNaN(c),g=m=>Number.isInteger(m)?String(m):String(Math.round(m*10)/10),v=[];if(this._triggerType==="threshold"){let m=parseFloat(this._triggerAbove),f=parseFloat(this._triggerBelow);if(isNaN(m)&&isNaN(f))return p;d&&v.push(s("trigger_hint_now",t).replace("{value}",g(c)+h)),isNaN(m)||v.push(s("trigger_hint_above",t).replace("{target}",g(m)+h)),isNaN(f)||v.push(s("trigger_hint_below",t).replace("{target}",g(f)+h))}else if(this._triggerType==="counter"){let m=parseFloat(this._triggerTargetValue);if(isNaN(m))return p;this._triggerDeltaMode?this._taskId?v.push(s("trigger_hint_counter_delta_edit",t).replace("{target}",g(m)+h)):d?v.push(s("trigger_hint_counter_delta",t).replace("{value}",g(c)+h).replace("{due}",g(c+m)+h).replace("{target}",g(m)+h)):v.push(s("trigger_hint_counter_delta_edit",t).replace("{target}",g(m)+h)):(d&&v.push(s("trigger_hint_now",t).replace("{value}",g(c)+h)),v.push(s("trigger_hint_counter_abs",t).replace("{target}",g(m)+h)))}else if(this._triggerType==="runtime"){let m=parseFloat(this._triggerRuntimeHours);if(isNaN(m))return p;v.push(s("trigger_hint_runtime",t).replace("{hours}",g(m))),v.push(s("trigger_hint_state_now",t).replace("{value}",String(i.state)))}else if(this._triggerType==="state_change"){let m=parseInt(this._triggerTargetChanges,10)||1,f=this._triggerToState.trim();v.push((f?s("trigger_hint_state_change_to",t).replace("{state}",f):s("trigger_hint_state_change",t)).replace("{count}",String(m))),v.push(s("trigger_hint_state_now",t).replace("{value}",String(i.state)))}return v.length?o`
${v.join(" ")}
`:p}_renderTriggerTypeFields(){let e=this._lang;return this._triggerType==="threshold"?o` + this._triggerAbove=i.target.value} + > + this._triggerBelow=i.target.value} + > + this._triggerEquals=i.target.value} + > + this._triggerNotEquals=i.target.value} + > + this._triggerForMinutes=i.target.value} + > + `:this._triggerType==="counter"?o` + this._triggerTargetValue=i.target.value} + > + + ${this._triggerDeltaMode?o` + this._triggerBaselineValue=i.target.value} + > +
+ ${this._taskId?s("baseline_start_help_edit",e):s("baseline_start_help",e)} + ${this._taskId&&this._liveBaselineValue!=null?o`
+ ${s("baseline_current_effective",e).replace("{value}",String(this._liveBaselineValue))} +
`:p} +
+ `:p} + `:this._triggerType==="state_change"?o` + ${this._renderStateField({label:s("from_state_optional",e),value:this._triggerFromState,entityId:this._triggerEntityId,onInput:i=>this._triggerFromState=i})} +
${s("state_value_help",e)}
+ ${this._renderStateField({label:s("to_state_optional",e),value:this._triggerToState,entityId:this._triggerEntityId,onInput:i=>this._triggerToState=i})} + this._triggerTargetChanges=i.target.value} + > +
${s("target_changes_help",e)}
+ this._triggerForMinutes=i.target.value} + > +
${s("for_minutes_state_help",e)}
+ `:this._triggerType==="runtime"?o` + this._triggerRuntimeHours=i.target.value} + > + ${this._renderOnStatesField({value:this._triggerOnStates,entityId:this._triggerEntityId,onInput:i=>this._triggerOnStates=i})} +
${s("runtime_on_states_help",e)}
+ `:p}render(){if(!this._open)return o``;let e=this._lang,i=this._taskId?s("edit_task",e):s("new_task",e);return o` + +
${i}
+
+ ${this._error?o`
${this._error}
`:p} + ${this._taskId===null&&this._objectChoices.length>0?o` +
+ + +
+ `:p} + this._name=t.target.value} + > +
+ + +
+ ${this._type==="reading"?o` + this._readingUnit=t.target.value} + > +
${s("reading_unit_help",e)}
+ `:p} + ${this._partsLoadFailed?o`
${s("parts_load_failed",e)}
`:p} + ${this.parts.length||this._foreignOwners.length?o` +
+ + ${this.parts.map(t=>this._renderConsumesRow(t))} + ${this._foreignOwners.length?o` +
+ ${s("shared_parts_other_objects",e)} +
${s("shared_parts_help",e)}
+ ${this._foreignOwners.map(t=>o` +
${t.name}
+ ${t.parts.map(r=>this._renderConsumesRow(r,t.entry_id))} + `)} +
+ `:p} +
+ `:p} +
+ + +
+
+ + this._labels=t.target.value} + /> +
${s("labels_help",e)}
+
+
+ + +
+ ${this._scheduleType==="time_based"?o` + this._intervalDays=t.target.value} + > + ${this._renderUnitSelect()} +
+ + +
+ ${this.scheduleTimeEnabled?o` + this._scheduleTime=t.target.value} + > + `:p} + `:p} + ${this._renderCalendarFields()} + ${this._scheduleType==="one_time"?o` + this._dueDate=t.target.value} + > + `:p} + ${this._renderRecurrenceExtras()} + ${this._renderSchedulePreview()} + this._warningDays=t.target.value} + > + this._earliestCompletionDays=t.target.value} + > + ${this.checklistsEnabled?o` +

${s("checklist_steps_optional",e)}

+ +
${s("checklist_help",e)}
+ `:p} + ${this._renderPhasesEditor(e)} +

${s("require_on_completion",e)}

+
+ ${k.map(t=>o` + + `)} +
+ this._lastPerformed=t.target.value} + > +
+ + +
+ ${this._availableUsers.length>=2?o` +
+ +
${s("shared_with_help",e)}
+
+ ${this._availableUsers.map(t=>o` + `)} +
+
+ ${this._assigneePool.length>=2?o` +
+ + +
`:p} + `:p} + ${this._renderTriggerFields()} + ${this._scheduleType==="sensor_based"?o` + ${this._entityPickerFallback?o` + this._environmentalEntity=t.target.value.trim()} + > + `:o` + s("environmental_entity_optional",e)} + .computeHelper=${()=>s("environmental_entity_helper",e)} + @value-changed=${t=>{this._environmentalEntity=(t.detail.value.environmental_entity||"").trim()}} + >`} + ${this._environmentalEntity?this._renderEnvironmentalAttribute(e):p} + `:p} + ${this._renderAdaptiveSection(e)} + this._notes=t.target.value} + > + this._documentationUrl=t.target.value} + > + this._customIcon=t.detail.value||""} + > + ${this._availableTags.length>0?o` +
+ + + +
+ `:o` + this._nfcTagId=t.target.value} + > +
+ ${s("nfc_tags_empty_help",e)} + ${s("nfc_tags_open_settings",e)} + · + +
+ `} + + ${this._requireTagScan?o`
${s("require_tag_scan_help",e)}
`:p} + + ${this._renderCompletionActionsSection(e)} +
+
+ ${s("cancel",e)} + + ${this._loading?s("saving",e):s("save",e)} + +
+
+ `}};n._PREVIEW_RELEVANT=new Set(["_open","_scheduleType","_intervalDays","_intervalUnit","_intervalAnchor","_dueDate","_weekdays","_nth","_nthWeekday","_domDay","_domLastDay","_domBusiness","_calOffset","_seasonMonths","_endsMode","_endsCount","_endsUntil","_lastPerformed"]),n.styles=L` + .dialog-title { + font-size: 18px; + font-weight: 500; + padding-bottom: 12px; + } + /* #129: entity/state pickers in the trigger form (ha-form + selector) */ + .entity-picker-form, + .state-picker-form { + display: block; + margin: 8px 0; + } + /* v1.3.0: completion-action sections (.adaptive-section shares the shell + but keeps its own class — tests count .ca-section elements) */ + .ca-section, + .adaptive-section { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 8px 12px; + margin-top: 8px; + } + .ca-section > summary, + .adaptive-section > summary { + cursor: pointer; + font-weight: 500; + } + .adaptive-section ms-textfield { + width: 100%; + margin-top: 8px; + display: block; + } + .adaptive-section label { + display: block; + margin-top: 8px; + } + .ca-section ms-textfield, + .ca-section ha-entity-picker, + .ca-section ha-service-picker, + .ca-section ha-form, + .ca-section .qc-feedback { + width: 100%; + margin-top: 8px; + display: block; + } + .ca-section .qc-feedback { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + .ca-test-row { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; + } + .ca-test-ok { color: var(--success-color, #4caf50); font-size: 13px; } + .ca-test-error { color: var(--error-color, #f44336); font-size: 13px; font-weight: 500; } + .ca-test-error-block { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } + .ca-test-error-detail { + font-size: 12px; + color: var(--secondary-text-color); + background: rgba(244, 67, 54, 0.08); + padding: 6px 8px; border-radius: 4px; + line-height: 1.4; + word-break: break-word; + } + .content { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 350px; + max-height: 70vh; + overflow-y: auto; + } + @media (max-width: 600px) { + .content { + min-width: 0; + max-height: none; + } + } + .dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + } + ms-textfield { + display: block; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .checklist-textarea { + width: 100%; + min-height: 88px; + padding: 8px; + font-family: inherit; + font-size: 14px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + resize: vertical; + box-sizing: border-box; + } + .consumes-row { + display: flex; + align-items: center; + gap: 8px; + padding: 2px 0; + } + .phase-def { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 8px; + margin: 6px 0; + } + .phase-def-head { + display: flex; + align-items: center; + gap: 8px; + } + .phase-def-head ms-textfield { + flex: 1; + min-width: 0; + } + .phase-part { + max-width: 160px; + padding: 6px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + } + .phase-qty { + width: 64px; + padding: 6px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + } + .phase-checklist { + 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); + } + .phase-seq-label { + font-size: 12px; + color: var(--secondary-text-color); + margin-top: 8px; + } + .phase-seq { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + padding: 4px 0; + } + .phase-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + background: var(--secondary-background-color); + font-size: 13px; + } + .phase-chip-x { + border: none; + background: none; + color: var(--secondary-text-color); + cursor: pointer; + padding: 0 2px; + font-size: 12px; + } + .phase-seq-add { + padding: 4px 8px; + border: 1px dashed var(--divider-color); + border-radius: 12px; + background: transparent; + color: var(--secondary-text-color); + font-size: 13px; + } + .consumes-check { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + } + .consumes-qty { + width: 64px; + padding: 4px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + } + /* #111: other objects' pools sit behind a disclosure so the object's OWN + parts stay the primary list; each group is headed by the owning object's + name, so which pool a checkbox means is never a guess. */ + .shared-pools { + margin-top: 6px; + } + .shared-pools > summary { + cursor: pointer; + padding: 2px 0; + font-size: 13px; + color: var(--secondary-text-color); + } + .shared-pool-owner { + margin-top: 6px; + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + } + .field-help { + font-size: 12px; + color: var(--secondary-text-color); + } + .baseline-effective { + margin-top: 2px; + font-weight: 500; + color: var(--primary-text-color); + } + /* Live computed trigger hint — reads the bound sensor and explains what + happens next. Info-accented so it reads as guidance, not an error. */ + .trigger-live-hint { + font-size: 12px; + color: var(--secondary-text-color); + border-left: 3px solid var(--info-color, #2196f3); + background: rgba(33, 150, 243, 0.08); + border-radius: 0 6px 6px 0; + padding: 6px 10px; + margin: 4px 0; + } + .field-help a, + .link-button { + background: none; + border: 0; + padding: 0; + color: var(--primary-color); + cursor: pointer; + font: inherit; + text-decoration: underline; + } + .field-help a:hover, + .link-button:hover { + text-decoration: none; + } + /* Smaller refresh icon-button when shown next to the dropdown. */ + .select-row .link-button { + margin-left: 8px; + text-decoration: none; + font-size: 16px; + } + .select-row .link-button:hover { + color: var(--primary-color); + opacity: 0.7; + } + h3 { + margin: 8px 0 0; + font-size: 14px; + color: var(--primary-color); + } + .select-row { + display: flex; + flex-direction: column; + gap: 4px; + } + .assignee-pool { + display: flex; + flex-wrap: wrap; + gap: 6px 14px; + margin-top: 4px; + } + .checkbox-row { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + cursor: pointer; + margin: 2px 0; + } + .checkbox-row input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .pool-item { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + } + .pool-item input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .select-row label { + font-size: 12px; + color: var(--secondary-text-color); + } + .select-row select { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .weekday-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .weekday-chip { + padding: 6px 12px; + border: 1px solid var(--divider-color); + border-radius: 16px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + } + .weekday-chip.selected { + background: var(--primary-color, #03a9f4); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color, #03a9f4); + } + .season-chip { + padding: 6px 10px; + border: 1px solid var(--divider-color); + border-radius: 16px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + } + .season-chip.selected { + background: var(--primary-color, #03a9f4); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color, #03a9f4); + } + .error { + color: var(--error-color, #f44336); + font-size: 13px; + } + .toggle-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + cursor: pointer; + } + `,a([$({attribute:!1})],n.prototype,"hass",2),a([$({type:Boolean,attribute:"checklists-enabled"})],n.prototype,"checklistsEnabled",2),a([$({type:Boolean,attribute:"schedule-time-enabled"})],n.prototype,"scheduleTimeEnabled",2),a([$({type:Boolean,attribute:"completion-actions-enabled"})],n.prototype,"completionActionsEnabled",2),a([$({type:Number,attribute:"default-warning-days"})],n.prototype,"defaultWarningDays",2),a([l()],n.prototype,"parts",2),a([l()],n.prototype,"_foreignOwners",2),a([l()],n.prototype,"_open",2),a([l()],n.prototype,"_entityPickerFallback",2),a([l()],n.prototype,"_loading",2),a([l()],n.prototype,"_error",2),a([l()],n.prototype,"_entryId",2),a([l()],n.prototype,"_taskId",2),a([l()],n.prototype,"_objectChoices",2),a([l()],n.prototype,"_name",2),a([l()],n.prototype,"_type",2),a([l()],n.prototype,"_scheduleType",2),a([l()],n.prototype,"_intervalDays",2),a([l()],n.prototype,"_intervalUnit",2),a([l()],n.prototype,"_dueDate",2),a([l()],n.prototype,"_warningDays",2),a([l()],n.prototype,"_earliestCompletionDays",2),a([l()],n.prototype,"_intervalAnchor",2),a([l()],n.prototype,"_weekdays",2),a([l()],n.prototype,"_nth",2),a([l()],n.prototype,"_nthWeekday",2),a([l()],n.prototype,"_domDay",2),a([l()],n.prototype,"_domLastDay",2),a([l()],n.prototype,"_domBusiness",2),a([l()],n.prototype,"_calOffset",2),a([l()],n.prototype,"_seasonMonths",2),a([l()],n.prototype,"_endsMode",2),a([l()],n.prototype,"_endsCount",2),a([l()],n.prototype,"_endsUntil",2),a([l()],n.prototype,"_schedulePreview",2),a([l()],n.prototype,"_schedulePreviewEnded",2),a([l()],n.prototype,"_notes",2),a([l()],n.prototype,"_documentationUrl",2),a([l()],n.prototype,"_customIcon",2),a([l()],n.prototype,"_priority",2),a([l()],n.prototype,"_labels",2),a([l()],n.prototype,"_enabled",2),a([l()],n.prototype,"_triggerEntityId",2),a([l()],n.prototype,"_triggerEntityIds",2),a([l()],n.prototype,"_triggerEntityLogic",2),a([l()],n.prototype,"_triggerAttribute",2),a([l()],n.prototype,"_triggerType",2),a([l()],n.prototype,"_triggerAbove",2),a([l()],n.prototype,"_triggerBelow",2),a([l()],n.prototype,"_triggerEquals",2),a([l()],n.prototype,"_triggerNotEquals",2),a([l()],n.prototype,"_triggerForMinutes",2),a([l()],n.prototype,"_triggerCombinator",2),a([l()],n.prototype,"_triggerTargetValue",2),a([l()],n.prototype,"_triggerDeltaMode",2),a([l()],n.prototype,"_triggerBaselineValue",2),a([l()],n.prototype,"_liveBaselineValue",2),a([l()],n.prototype,"_autoCompleteOnRecovery",2),a([l()],n.prototype,"_triggerFromState",2),a([l()],n.prototype,"_triggerToState",2),a([l()],n.prototype,"_triggerTargetChanges",2),a([l()],n.prototype,"_triggerRuntimeHours",2),a([l()],n.prototype,"_triggerOnStates",2),a([l()],n.prototype,"_compoundLogic",2),a([l()],n.prototype,"_compoundConditions",2),a([l()],n.prototype,"_suggestedAttributes",2),a([l()],n.prototype,"_availableAttributes",2),a([l()],n.prototype,"_entityDomain",2),a([l()],n.prototype,"_lastPerformed",2),a([l()],n.prototype,"_nfcTagId",2),a([l()],n.prototype,"_requireTagScan",2),a([l()],n.prototype,"_readingUnit",2),a([l()],n.prototype,"_consumesParts",2),a([l()],n.prototype,"_partsLoadFailed",2),a([l()],n.prototype,"_availableTags",2),a([l()],n.prototype,"_responsibleUserId",2),a([l()],n.prototype,"_assigneePool",2),a([l()],n.prototype,"_rotationStrategy",2),a([l()],n.prototype,"_availableUsers",2),a([l()],n.prototype,"_checklistText",2),a([l()],n.prototype,"_phaseDefs",2),a([l()],n.prototype,"_phaseSeq",2),a([l()],n.prototype,"_requiredCompletion",2),a([l()],n.prototype,"_scheduleTime",2),a([l()],n.prototype,"_actionService",2),a([l()],n.prototype,"_actionTargetEntity",2),a([l()],n.prototype,"_actionData",2),a([l()],n.prototype,"_actionDataJsonFallback",2),a([l()],n.prototype,"_actionTesting",2),a([l()],n.prototype,"_actionTestResult",2),a([l()],n.prototype,"_actionTestError",2),a([l()],n.prototype,"_qcNotes",2),a([l()],n.prototype,"_qcCost",2),a([l()],n.prototype,"_qcDuration",2),a([l()],n.prototype,"_qcFeedback",2),a([l()],n.prototype,"_environmentalEntity",2),a([l()],n.prototype,"_environmentalAttribute",2),a([l()],n.prototype,"_adaptiveEnabled",2),a([l()],n.prototype,"_adaptiveAlpha",2),a([l()],n.prototype,"_adaptiveMin",2),a([l()],n.prototype,"_adaptiveMax",2),a([l()],n.prototype,"_adaptiveSeasonal",2),a([l()],n.prototype,"_adaptivePrediction",2),a([l()],n.prototype,"_conditionAttrOptions",2);var P=n;customElements.get("maintenance-task-dialog")||customElements.define("maintenance-task-dialog",P);export{P as a}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/complete-dialog-AMDNGPVQ.js b/custom_components/maintenance_supporter/frontend/panel-chunks/complete-dialog-AMDNGPVQ.js new file mode 100644 index 00000000..e7ac45b1 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/complete-dialog-AMDNGPVQ.js @@ -0,0 +1,2 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-SBOLPJAX.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VFPFVFKO.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-KKI2ZPWC.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";export{a as MaintenanceCompleteDialog}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/dialog-mount-AQCAYISH.js b/custom_components/maintenance_supporter/frontend/panel-chunks/dialog-mount-AQCAYISH.js new file mode 100644 index 00000000..09108b51 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/dialog-mount-AQCAYISH.js @@ -0,0 +1,2 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{A as k,q as a,r as b,s as c,t as d,u as e,v as f,w as g,x as h,y as i,z as j}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-S2O7XNSC.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JSY4RS7D.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VZ2E6UPO.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-SYT5D73A.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-SBOLPJAX.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VFPFVFKO.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-MKNEWK2D.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-3L2PFMVS.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-J4OBOTUE.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-KKI2ZPWC.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";export{b as __resetSettingsCacheForTests,a as getRowActionStyle,h as openCompleteDialog,c as openCreateObjectDialog,e as openCreateTaskDialog,d as openEditObjectDialog,f as openEditTaskDialog,g as openHistoryEditDialog,k as openObjectQuickActions,i as openQrDialog,j as openTaskQuickActions}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/object-dialog-MCCSC5BT.js b/custom_components/maintenance_supporter/frontend/panel-chunks/object-dialog-MCCSC5BT.js new file mode 100644 index 00000000..357ee5cb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/object-dialog-MCCSC5BT.js @@ -0,0 +1,2 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JSY4RS7D.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-SYT5D73A.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-KKI2ZPWC.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";export{a as MaintenanceObjectDialog}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/qr-dialog-CCPP2WKW.js b/custom_components/maintenance_supporter/frontend/panel-chunks/qr-dialog-CCPP2WKW.js new file mode 100644 index 00000000..f7c09e42 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/qr-dialog-CCPP2WKW.js @@ -0,0 +1,2 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MKNEWK2D.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-3L2PFMVS.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";export{a as MaintenanceQrDialog}; diff --git a/custom_components/maintenance_supporter/frontend/panel-chunks/settings-view-FAYNKL6P.js b/custom_components/maintenance_supporter/frontend/panel-chunks/settings-view-FAYNKL6P.js new file mode 100644 index 00000000..db0cadac --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/panel-chunks/settings-view-FAYNKL6P.js @@ -0,0 +1,1206 @@ +/*! maintenance_supporter frontend 2.70.0 */ +import{a as I,f as E,h as T}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-UGIIDVTL.js";import{a as P,b as H}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3L2PFMVS.js";import{a as z}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-J4OBOTUE.js";import{a as c,b as q,c as o,e as b,f as p,g as S,h as A,l as x,m as d,q as s,s as j}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VSV22GFT.js";var u={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},w=g=>(..._)=>({_$litDirective$:g,values:_}),f=class{constructor(_){}get _$AU(){return this._$AM._$AU}_$AT(_,e,t){this._$Ct=_,this._$AM=e,this._$Ci=t}_$AS(_,e){return this.update(_,e)}update(_,e){return this.render(...e)}};var y=class extends f{constructor(_){if(super(_),this.it=p,_.type!==u.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(_){if(_===p||_==null)return this._t=void 0,this.it=_;if(_===b)return _;if(typeof _!="string")throw Error(this.constructor.directiveName+"() called with a non-string value");if(_===this.it)return this._t;this.it=_;let e=[_];return e.raw=e,this._t={_$litType$:this.constructor.resultType,strings:e,values:[]}}};y.directiveName="unsafeHTML",y.resultType=1;var M=w(y);var{I:Y}=S;var C=g=>g.strings===void 0;var R={},O=(g,_=R)=>g._$AH=_;var $=w(class extends f{constructor(g){if(super(g),g.type!==u.PROPERTY&&g.type!==u.ATTRIBUTE&&g.type!==u.BOOLEAN_ATTRIBUTE)throw Error("The `live` directive is not allowed on child or event bindings");if(!C(g))throw Error("`live` bindings can only contain a single expression")}render(g){return g}update(g,[_]){if(_===b||_===p)return _;let e=g.element,t=g.name;if(g.type===u.PROPERTY){if(_===e[t])return b}else if(g.type===u.BOOLEAN_ATTRIBUTE){if(!!_===e.hasAttribute(t))return b}else if(g.type===u.ATTRIBUTE&&e.getAttribute(t)===_+"")return b;return O(g),_}});var B=["EUR","USD","GBP","JPY","CHF","CAD","AUD","NZD","CNY","INR","BRL","CZK","PLN","RUB","SEK","NOK","DKK","UAH"],l=class extends A{constructor(){super(...arguments);this.budget=null;this._settings=null;this._loading=!0;this._importCsv="";this._importLoading=!1;this._includeHistory=!0;this._toast="";this._testingNotification=!1;this._personTargets=[];this._testingUser="";this._users=[];this._savedViews=[];this._vacEnabled=!1;this._vacStart="";this._vacEnd="";this._vacBuffer=3;this._vacExempt=new Set;this._vacIsActive=!1;this._vacWindowEnd=null;this._vacAllTasks=[];this._vacPreview=[];this._vacPreviewLoading=!1;this._vacSaving=!1;this._qrObjects=[];this._qrSelectedEntries=new Set;this._qrActions=new Set(["view"]);this._qrUrlMode="companion";this._qrBatchLoading=!1;this._qrBatchResults=[];this._qrObjectsLoaded=!1;this._exportObjects=[];this._exportSelectedEntries=new Set;this._exportObjectsLoaded=!1;this._docArchiveLoading=!1;this._loaded=!1;this._userService=null;this._sendTestNotification=async e=>{e?this._testingUser=e:this._testingNotification=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/test_notification",...e?{user_id:e}:{}}),a=t.message||(t.success?s("test_notification_success",this._lang):s("test_notification_failed",this._lang));this._showToast(a)}catch{this._showToast(s("test_notification_failed",this._lang))}finally{e?this._testingUser="":this._testingNotification=!1}};this._allTemplates=[];this._templateCategories={};this._tplOpenGroups=new Set;this._templatesRequested=!1}get _lang(){return j(this.hass)}updated(e){super.updated(e),e.has("hass")&&this.hass&&!this._loaded?(this._loaded=!0,this._userService=new z(this.hass),this._loadSettings(),this._loadUsers()):e.has("hass")&&this.hass&&this._userService&&this._userService.updateHass(this.hass)}async _loadUsers(){if(this._userService){try{this._users=await this._userService.getUsers()}catch{this._users=[]}this._loadNotifyTargets()}}async _loadNotifyTargets(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/notify/user_targets"});this._personTargets=e.targets||[]}catch{this._personTargets=[]}}async _loadSettings(){this._loading=!0;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/settings"});this._settings=e,this._hydrateVacationFromSettings()}catch{}try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/views/list"});this._savedViews=e.views||[]}catch{}this._loading=!1}_hydrateVacationFromSettings(){let e=this._settings?.vacation;e&&(this._vacEnabled=e.enabled,this._vacStart=e.start||"",this._vacEnd=e.end||"",this._vacBuffer=e.buffer_days,this._vacExempt=new Set(e.exempt_task_ids||[]),this._vacIsActive=e.is_active,this._vacWindowEnd=e.window_end)}_renderBatteryNotesHint(e){let t=this._settings?.general?.battery_notes;if(!t||!t.devices)return p;let a=this._settings?.general?.battery_low_percent??20,i=t.default>a,n=s("bn_summary",e).replace("{name}","Battery Notes").replace("{pct}",String(t.default)).replace("{n}",String(t.devices)),r=(i?s("bn_above_floor",e).replace("{name}","Battery Notes"):s("bn_floor_decides",e)).replace("{floor}",String(a));return o` +
+ + + ${n.split(":")[0]}${n.slice(n.indexOf(":"))} + ${t.overrides.length?o` · ${s("bn_overrides",e).replace("{n}",String(t.overrides.length+t.more))} + ${t.overrides.map((h,m)=>o`${m?" \xB7 ":" "}${h.device_id?o`${h.name}`:h.name} (${h.threshold} %)`)} + ${t.more?o` ${s("bn_more",e).replace("{n}",String(t.more))}`:p}`:p} + ${r} + +
`}async _updateSetting(e,t){try{let a=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:{[e]:t}});this._settings=a,this._showToast(s("settings_saved",this._lang)),this.dispatchEvent(new CustomEvent("settings-changed"))}catch{this._showToast(s("action_error",this._lang)),this.requestUpdate()}}_showToast(e){this._toast=e,setTimeout(()=>{this._toast=""},3e3)}_downloadFile(e,t,a){P(e,t,a)}render(){let e=this._lang;return this._loading||!this._settings?o`
Loading…
`:o` + ${this._renderFeatures(e)} + ${this._renderPanelAccess(e)} + ${this._renderGeneral(e)} + ${this._renderObjectsColumns(e)} + ${this._settings.general.notifications_enabled?this._renderNotifications(e):p} + ${this.features.budget?this._renderBudget(e):p} + ${this._renderArchive(e)} + ${this._renderVacation(e)} + ${this._renderPrintQr(e)} + ${this._renderImportExport(e)} + ${this._renderTemplateToggles(e)} + ${this._toast?o`
${this._toast}
`:p} + `}scrollToSection(e){requestAnimationFrame(()=>{let t=this.shadowRoot;if(!t)return;let a=t.querySelector(`[data-section="${e}"]`)??t.querySelector(`[data-section-alt="${e}"]`);a&&a.scrollIntoView({behavior:"smooth",block:"start"})})}_renderPanelAccess(e){let t=new Set(this._settings.admin_panel_user_ids||[]),a=this._users.filter(r=>!r.is_admin),i=this._settings.operator_write_enabled??!1,n=(r,h)=>{let m=new Set(t);h?m.add(r):m.delete(r),this._updateSetting("admin_panel_user_ids",[...m])};return o` +
+

${s("settings_panel_access",e)} ${i&&t.size>0?o`${t.size}`:p}

+

${s("settings_panel_access_desc",e)}

+ + ${i?a.length===0?o`
${s("no_non_admin_users",e)}
`:a.map(r=>o` + + `):p} +
+ `}_renderFeatures(e){let t=this._settings.features,a=[{key:"adaptive",settingKey:"advanced_adaptive_visible",label:s("feat_adaptive",e),desc:s("feat_adaptive_desc",e)},{key:"predictions",settingKey:"advanced_predictions_visible",label:s("feat_predictions",e),desc:s("feat_predictions_desc",e)},{key:"seasonal",settingKey:"advanced_seasonal_visible",label:s("feat_seasonal",e),desc:s("feat_seasonal_desc",e)},{key:"environmental",settingKey:"advanced_environmental_visible",label:s("feat_environmental",e),desc:s("feat_environmental_desc",e)},{key:"budget",settingKey:"advanced_budget_visible",label:s("feat_budget",e),desc:s("feat_budget_desc",e)},{key:"groups",settingKey:"advanced_groups_visible",label:s("feat_groups",e),desc:s("feat_groups_desc",e)},{key:"checklists",settingKey:"advanced_checklists_visible",label:s("feat_checklists",e),desc:s("feat_checklists_desc",e)},{key:"schedule_time",settingKey:"advanced_schedule_time_visible",label:s("feat_schedule_time",e),desc:s("feat_schedule_time_desc",e)},{key:"completion_actions",settingKey:"advanced_completion_actions_visible",label:s("feat_completion_actions",e),desc:s("feat_completion_actions_desc",e)}];return o` +
+

${s("settings_features",e)}

+

${s("settings_features_desc",e)}

+ ${a.map(i=>o` + + `)} +
+ `}async _loadTemplates(){if(!this._templatesRequested){this._templatesRequested=!0;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/templates",language:this._lang});this._allTemplates=e.templates||[],this._templateCategories=e.categories||{}}catch{}}}_renderTemplateToggles(e){this._loadTemplates();let t=new Set(this._settings.disabled_template_ids||[]),a=new Map;for(let n of Object.keys(this._templateCategories))a.set(n,[]);for(let n of this._allTemplates)a.has(n.category)||a.set(n.category,[]),a.get(n.category).push(n);let i=n=>this._templateCategories[n]?.["name_"+e]||this._templateCategories[n]?.name_en||n;return o` +
+

${s("settings_templates_label",e)}

+

${s("settings_templates_hint",e)}

+ ${[...a.entries()].filter(([,n])=>n.length>0).map(([n,r])=>{let h=r.filter(v=>!t.has(v.id)).length,m=this._tplOpenGroups.has(n);return o` +
+
this._toggleTplGroupOpen(n)} + @keydown=${v=>{(v.key==="Enter"||v.key===" ")&&(v.preventDefault(),this._toggleTplGroupOpen(n))}} + > + + + ${i(n)} + ${h}/${r.length} + v.stopPropagation()} + @change=${v=>this._toggleTemplateGroup(r.map(k=>k.id),v.target.checked)} + /> +
+ ${m?r.map(v=>o` + + `):p} +
+ `})} +
+ `}_toggleTemplate(e,t){let a=new Set(this._settings.disabled_template_ids||[]);t?a.delete(e):a.add(e),this._updateSetting("disabled_template_ids",[...a])}_toggleTplGroupOpen(e){let t=new Set(this._tplOpenGroups);t.has(e)?t.delete(e):t.add(e),this._tplOpenGroups=t}_toggleTemplateGroup(e,t){let a=new Set(this._settings.disabled_template_ids||[]);for(let i of e)t?a.delete(i):a.add(i);this._updateSetting("disabled_template_ids",[...a])}_renderObjectsColumns(e){let t=T(this._settings.objects_table_columns);return o` +
+

${s("objects_table_columns_label",e)}

+

${s("objects_table_columns_hint",e)}

+ ${E.map(a=>o` + + `)} +
+ `}_toggleColumn(e,t){let a=new Set(T(this._settings.objects_table_columns));t?a.add(e):a.delete(e);let i=E.filter(n=>n.required||a.has(n.key)).map(n=>n.key);this._updateSetting("objects_table_columns",i)}_renderGeneral(e){let t=this._settings.general,a=t.notify_targets??[],i=this._settings.budget;return o` +
+

${s("settings_general",e)}

+ + + + ${this._renderBatteryNotesHint(e)} +
${s("settings_thresholds_hint",e)}
+ + + + ${t.panel_enabled?o` + + `:""} + +
${s("settings_install_assist_sentences_hint",e)}
+ + ${t.notifications_enabled?o` + +
+ ${s("test_notification",e)} + +
+ ${this._personTargets.length?o` +
+ ${s("notify_per_person",e)} + ${this._personTargets.map(n=>o` +
+ ${n.name} + + ${n.services.length?n.services.join(", "):s("notify_no_own_device",e)} + + +
+ `)} +
+ `:p} + `:p} + + +
+ `}_todoEntities(e){let t=Object.keys(this.hass?.states||{}).filter(a=>a.startsWith("todo.")).sort();return e&&!t.includes(e)&&t.unshift(e),t}_renderNotifications(e){let t=this._settings.notifications,a=this._settings.actions;return o` +
+

${s("settings_notifications",e)}

+ + + ${t.due_soon_enabled?o` + + `:p} + + + ${t.overdue_enabled?o` + + `:p} + + + ${t.triggered_enabled?o` + + `:p} + + + ${t.quiet_hours_enabled?o` +
+ ${s("settings_quiet_start",e)} + this._updateSetting("quiet_hours_start",i.target.value)} /> +
+
+ ${s("settings_quiet_end",e)} + this._updateSetting("quiet_hours_end",i.target.value)} /> +
+ `:p} + + + + + ${t.bundling_enabled?o` + + `:p} + +
${s("settings_reminder_leads_hint",e)}
+ +
${s("settings_notify_scope_hint",e)}
+ +

${s("settings_actions",e)}

+ + + + ${a.snooze_enabled?o` + + `:p} + +
${s("settings_weekly_digest_hint",e)}
+ + ${a.warranty_reminder_enabled?o` + + `:p} +
${s("settings_warranty_reminder_hint",e)}
+
+ `}_renderBudget(e){let t=this._settings.budget;return o` +
+

${s("settings_budget",e)}

+ + + + ${t.alerts_enabled?o` + + `:p} +
+ `}_renderArchive(e){let t=this._settings.archive??{oneoff_days:14,delete_archived_oneoff_days:0};return o` +
+

${s("settings_archive",e)}

+

${s("settings_archive_desc",e)}

+ + +
+ `}_renderVacation(e){let t=this._vacEnabled&&!this._vacIsActive&&this._vacWindowEnd&&new Date(this._vacWindowEnd) +

+ ${s("vacation_title",e)} + ${this._vacIsActive?o`${s("vacation_active",e)}`:p} + ${t?o`${s("vacation_ended",e)}`:p} +

+

${s("vacation_desc",e)}

+ + + +
+ + + +
+ +
+ + ${s("vacation_exempt_title",e)} + ${a>0?o`${a}`:p} + +

${s("vacation_exempt_desc",e)}

+ ${this._vacAllTasks.length===0?o``:o` +
+ ${this._renderVacationTaskList(e)} +
+ `} +
+ + ${this._vacStart&&this._vacEnd?o` +
+ + ${this._vacPreview.length>0?o`${this._vacPreview.length} ${s("vacation_preview_affected",e)}`:p} +
+ ${this._vacPreview.length>0?this._renderVacationPreview(e):p} + `:p} + + ${this._vacIsActive||t?o``:p} + + `}_renderVacationTaskList(e){let t=new Map;for(let i of this._vacAllTasks){let n=t.get(i.object_name)||[];n.push(i),t.set(i.object_name,n)}return[...t.entries()].sort(([i],[n])=>i.localeCompare(n)).map(([i,n])=>o` +
+
${i||s("no_objects",e)}
+ ${n.sort((r,h)=>r.task_name.localeCompare(h.task_name)).map(r=>o` + + `)} +
+ `)}_renderVacationPreview(e){return o` +
+ ${this._vacPreview.map(t=>{let a=t.events.map(n=>{let r=`vacation_event_${n.status}`;return`${n.date} (${s(r,e)})`}).join(" \xB7 "),i=!t.will_suppress;return o` +
+
+
+ ${t.object_name} · ${t.task_name} + ${t.kind==="sensor_based"?o`${s("vacation_sensor_based",e)}`:p} +
+
${a}
+
+
+ + ${t.kind==="time_based"?o``:p} + +
+
+ `})} +
+ `}async _loadAllTasksForVacation(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}),t=[];for(let a of e.objects||[])for(let i of a.tasks||[])t.push({entry_id:a.entry_id,object_name:a.object.name||"",task_id:i.id,task_name:i.name||""});this._vacAllTasks=t}catch{this._showToast(s("action_error",this._lang))}}async _saveVacation(e){if(!this._vacSaving){this._vacSaving=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",...e});this._vacEnabled=t.enabled,this._vacStart=t.start||"",this._vacEnd=t.end||"",this._vacBuffer=t.buffer_days,this._vacExempt=new Set(t.exempt_task_ids||[]),this._vacIsActive=t.is_active,this._vacWindowEnd=t.window_end,this.dispatchEvent(new CustomEvent("settings-changed"))}catch(t){let a=t?.message||s("action_error",this._lang);this._showToast(a)}finally{this._vacSaving=!1}}}_toggleVacationEnabled(e){this._saveVacation({enabled:e})}_setVacationDate(e,t){let a={};a[e]=t||null,this._saveVacation(a)}_setVacationBuffer(e){e<0||e>14||this._saveVacation({buffer_days:e})}_toggleVacationExempt(e,t){let a=new Set(this._vacExempt);t?a.add(e):a.delete(e),this._saveVacation({exempt_task_ids:[...a]})}async _loadVacationPreview(){this._vacPreviewLoading=!0;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/preview"});this._vacPreview=e.rows||[]}catch{this._showToast(s("action_error",this._lang))}finally{this._vacPreviewLoading=!1}}async _previewActionComplete(e){try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/complete",entry_id:e.entry_id,task_id:e.task_id}),this._showToast(s("vacation_marked_complete",this._lang)),await this._loadVacationPreview()}catch{this._showToast(s("action_error",this._lang))}}async _previewActionSkip(e){try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/skip",entry_id:e.entry_id,task_id:e.task_id,reason:"Skipped before vacation"}),this._showToast(s("vacation_marked_skip",this._lang)),await this._loadVacationPreview()}catch{this._showToast(s("action_error",this._lang))}}async _endVacationNow(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._vacEnabled=e.enabled,this._vacEnd=e.end||"",this._vacIsActive=e.is_active,this._vacWindowEnd=e.window_end,this.dispatchEvent(new CustomEvent("settings-changed")),this._showToast(s("vacation_ended",this._lang))}catch{this._showToast(s("action_error",this._lang))}}_renderPrintQr(e){let t=this._qrSelectedEntries.size||this._qrObjects.length,a=this._qrActions.size,i=t*a,n=i>200;return o` +
+

${s("qr_print_title",e)}

+

${s("qr_print_desc",e)}

+ + ${this._qrObjectsLoaded?o` +
+ ${s("qr_print_filter",e)} + +
+
${s("qr_print_objects",e)}
+
+ ${this._qrObjects.length===0?o`
${s("no_objects",e)}
`:this._qrObjects.map(r=>o` + + `)} +
+
+ +
+
${s("qr_print_actions",e)}
+
+ ${["view","complete","skip"].map(r=>o` + + `)} +
+
+ +
+
${s("qr_print_url_mode",e)}
+ +
+ +
+
+ ${s("qr_print_estimate",e)}: ${i} + ${n?o` — ${s("qr_print_over_limit",e)}`:p} +
+ +
+
+ + ${this._qrBatchResults.length>0?o` +
+ ${this._qrBatchResults.length} ${s("qr_print_ready",e)} + +
+
+ ${this._qrBatchResults.map(r=>o` +
+
${M(r.svg)}
+
+
${r.object_name}
+
${r.task_name}
+
${s("qr_action_"+r.action,e)}
+
+
+ `)} +
+ `:p} + `:o``} +
+ `}async _loadQrObjects(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"});this._qrObjects=(e.objects||[]).map(t=>({entry_id:t.entry_id,name:t.object.name,task_count:(t.tasks||[]).length})).sort((t,a)=>t.name.localeCompare(a.name)),this._qrObjectsLoaded=!0}catch{this._showToast(s("action_error",this._lang))}}_toggleQrObject(e,t){let a=new Set(this._qrSelectedEntries);if(a.size===0)for(let i of this._qrObjects)a.add(i.entry_id);t?a.add(e):a.delete(e),a.size===this._qrObjects.length&&a.clear(),this._qrSelectedEntries=a}_toggleQrAction(e,t){let a=new Set(this._qrActions);t?a.add(e):a.delete(e),this._qrActions=a}async _generateBatch(){this._qrBatchLoading=!0,this._qrBatchResults=[];try{let e={type:"maintenance_supporter/qr/batch_generate",actions:[...this._qrActions],url_mode:this._qrUrlMode};this._qrSelectedEntries.size>0&&(e.entry_ids=[...this._qrSelectedEntries]);let t=await this.hass.connection.sendMessagePromise(e);this._qrBatchResults=t.qrs||[],this._qrBatchResults.length===0&&this._showToast(s("qr_print_empty",this._lang))}catch(e){let t=e?.message||s("action_error",this._lang);this._showToast(t)}finally{this._qrBatchLoading=!1}}_printQrs(){if(this._qrBatchResults.length===0)return;let e=this._lang,t=this._qrBatchResults.map(r=>{let h=s("qr_action_"+r.action,e);return` +
+
${r.svg}
+
+
${this._escapeHtml(r.object_name)}
+
${this._escapeHtml(r.task_name)}
+
${this._escapeHtml(h)}
+
+
`}).join(""),a=s("qr_print_title",e),i=` + + + + ${this._escapeHtml(a)} + + + +
+

${this._escapeHtml(a)} \u2014 ${this._qrBatchResults.length}

+ +
+
${t}
+