New apps Added
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
"""Frontend registration for TaskMate custom cards."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.frontend import add_extra_js_url
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# URL base path for serving static files
|
||||
URL_BASE: Final = "/taskmate"
|
||||
|
||||
# Lovelace resources. Listed in load order: shared utilities first so that
|
||||
# window.__taskmate_attrs / __taskmate_localize are reliably defined by the
|
||||
# time card modules execute. Without this, a cold-cache load could render
|
||||
# cards before the utility globals existed, producing empty chore lists and
|
||||
# raw localisation keys for non-admin users.
|
||||
CARDS: Final = [
|
||||
"taskmate-attr-resolver.js",
|
||||
"taskmate-localize.js",
|
||||
"taskmate-design.js",
|
||||
"taskmate-badges-card.js",
|
||||
"taskmate-child-card.js",
|
||||
"taskmate-rewards-card.js",
|
||||
"taskmate-approvals-card.js",
|
||||
"taskmate-points-card.js",
|
||||
"taskmate-reorder-card.js",
|
||||
"taskmate-overview-card.js",
|
||||
"taskmate-activity-card.js",
|
||||
"taskmate-streak-card.js",
|
||||
"taskmate-weekly-card.js",
|
||||
"taskmate-graph-card.js",
|
||||
"taskmate-reward-progress-card.js",
|
||||
"taskmate-leaderboard-card.js",
|
||||
"taskmate-parent-dashboard-card.js",
|
||||
"taskmate-penalties-card.js",
|
||||
"taskmate-bonuses-card.js",
|
||||
"taskmate-points-display-card.js",
|
||||
"taskmate-calendar-card.js",
|
||||
"taskmate-photo-gallery-card.js",
|
||||
"taskmate-family-goal-card.js",
|
||||
]
|
||||
|
||||
# Cards that USED to ship but were removed. Their files no longer exist, so any
|
||||
# Lovelace resource still registered for them 404s on every dashboard load for
|
||||
# users upgrading from a version that had them. We deregister these by EXACT
|
||||
# URL only (never a heuristic diff — that once wiped every resource, which is
|
||||
# why blanket stale-cleanup was removed from async_register_cards).
|
||||
RETIRED_CARDS: Final = [
|
||||
"taskmate-task-groups-card.js", # removed #452
|
||||
"taskmate-templates-card.js", # removed #448
|
||||
"taskmate-reminders-card.js", # removed #450
|
||||
]
|
||||
|
||||
# JS modules loaded on every HA frontend page (config flow sound preview).
|
||||
# taskmate-localize.js is ALSO listed in CARDS (Lovelace resources) so it loads
|
||||
# early on dashboards; it must be here too because the admin panel (panel.py) is
|
||||
# a panel_custom page, not a Lovelace dashboard, and never loads Lovelace
|
||||
# resources — without this the panel renders raw i18n keys. Double-loading is a
|
||||
# no-op: the module is keyed by URL (loaded once) and only assigns idempotent
|
||||
# window.__taskmate_localize globals.
|
||||
GLOBAL_MODULES: Final = [
|
||||
"taskmate-config-sounds.js",
|
||||
"taskmate-localize.js",
|
||||
# Like localize.js above, the admin panel (a panel_custom page, not a
|
||||
# Lovelace dashboard) never loads Lovelace resources, so the shared design
|
||||
# layer must be loaded globally for window.__taskmate_design to exist when
|
||||
# the panel resolves/stamps its design. Double-loading is a no-op (keyed by
|
||||
# URL, assigns idempotent globals).
|
||||
"taskmate-design.js",
|
||||
]
|
||||
|
||||
# Track if frontend is registered
|
||||
FRONTEND_REGISTERED: Final = "frontend_registered"
|
||||
|
||||
|
||||
async def _async_get_version(hass: HomeAssistant) -> str:
|
||||
"""Get version from manifest.json for cache busting (async-safe)."""
|
||||
manifest_path = Path(__file__).parent / "manifest.json"
|
||||
try:
|
||||
content = await hass.async_add_executor_job(
|
||||
manifest_path.read_text, "utf-8"
|
||||
)
|
||||
return json.loads(content).get("version", "1.0.0")
|
||||
except (OSError, json.JSONDecodeError, AttributeError):
|
||||
return "1.0.0"
|
||||
|
||||
|
||||
async def async_register_frontend(hass: HomeAssistant) -> None:
|
||||
"""Register static paths for serving card JavaScript files."""
|
||||
# Only register once
|
||||
if hass.data.get(DOMAIN, {}).get(FRONTEND_REGISTERED):
|
||||
_LOGGER.debug("Frontend already registered, skipping")
|
||||
return
|
||||
|
||||
www_path = Path(__file__).parent / "www"
|
||||
|
||||
if not www_path.exists():
|
||||
_LOGGER.warning("www directory not found at %s", www_path)
|
||||
return
|
||||
|
||||
# Register the www folder as a static path
|
||||
await hass.http.async_register_static_paths(
|
||||
[StaticPathConfig(URL_BASE, str(www_path), False)]
|
||||
)
|
||||
|
||||
_LOGGER.debug("Registered static path: %s -> %s", URL_BASE, www_path)
|
||||
|
||||
# Authenticated upload/serve endpoints for chore evidence photos.
|
||||
from .http_photos import async_register_photo_views
|
||||
async_register_photo_views(hass)
|
||||
|
||||
# Token-gated ICS calendar feed (FEAT-10).
|
||||
from .http_calendar import async_register_calendar_view
|
||||
async_register_calendar_view(hass)
|
||||
|
||||
# Register global JS modules (loaded on all pages, including config flow)
|
||||
version = await _async_get_version(hass)
|
||||
for module in GLOBAL_MODULES:
|
||||
module_url = f"{URL_BASE}/{module}?v={version}"
|
||||
add_extra_js_url(hass, module_url)
|
||||
_LOGGER.info("Registered global frontend module: %s", module_url)
|
||||
|
||||
# Mark as registered
|
||||
hass.data.setdefault(DOMAIN, {})[FRONTEND_REGISTERED] = True
|
||||
|
||||
|
||||
async def async_register_cards(hass: HomeAssistant) -> None:
|
||||
"""Register and version-update TaskMate Lovelace resources on every startup.
|
||||
|
||||
Safety rules — this function will ONLY ever:
|
||||
1. Add missing TaskMate cards (create)
|
||||
2. Update the ?v= query string on existing TaskMate cards (update)
|
||||
It will NEVER delete any resource. Stale cleanup is removed entirely
|
||||
because URL mismatches caused accidental deletion of all resources.
|
||||
Only URLs that begin with /taskmate/ are ever touched.
|
||||
"""
|
||||
version = await _async_get_version(hass)
|
||||
_LOGGER.info("TaskMate resource manager: version=%s", version)
|
||||
|
||||
lovelace_data = hass.data.get("lovelace")
|
||||
if lovelace_data is None:
|
||||
_LOGGER.warning("TaskMate: Lovelace not available — skipping resource registration.")
|
||||
return
|
||||
|
||||
mode = getattr(lovelace_data, "mode", "storage")
|
||||
if mode == "yaml":
|
||||
_LOGGER.info("TaskMate: Lovelace YAML mode — add resources manually:")
|
||||
for card in CARDS:
|
||||
_LOGGER.info(" - url: %s/%s?v=%s (type: module)", URL_BASE, card, version)
|
||||
return
|
||||
|
||||
try:
|
||||
resources = lovelace_data.resources
|
||||
if resources is None:
|
||||
_LOGGER.warning("TaskMate: Lovelace resources object not available.")
|
||||
return
|
||||
|
||||
# Force load storage from disk BEFORE reading items.
|
||||
# Without this, async_items() returns empty if storage hasn't been
|
||||
# read yet — causing us to create duplicate entries which then get
|
||||
# wiped when lovelace subsequently loads its own storage file.
|
||||
#
|
||||
# Browser Mod uses: resources.async_load() + resources.loaded flag
|
||||
# WebRTC uses: resources.async_get_info()
|
||||
# We use both as a belt-and-braces approach.
|
||||
if hasattr(resources, "async_load"):
|
||||
await resources.async_load()
|
||||
if hasattr(resources, "async_get_info"):
|
||||
await resources.async_get_info()
|
||||
|
||||
# Build a map of base_url (without ?v=...) -> full resource item
|
||||
# ONLY for resources whose URL starts with /taskmate/
|
||||
# Everything else is completely ignored
|
||||
existing: dict[str, dict] = {}
|
||||
all_items = list(resources.async_items())
|
||||
_LOGGER.debug("TaskMate: total Lovelace resources = %d", len(all_items))
|
||||
|
||||
for item in all_items:
|
||||
url = item.get("url", "")
|
||||
base_url = url.split("?")[0]
|
||||
if base_url.startswith(URL_BASE + "/"):
|
||||
existing[base_url] = item
|
||||
_LOGGER.debug("TaskMate: found existing resource: %s", url)
|
||||
|
||||
_LOGGER.info("TaskMate: found %d existing TaskMate resources", len(existing))
|
||||
|
||||
# Add missing cards or update version on existing ones
|
||||
# NEVER delete anything
|
||||
for card in CARDS:
|
||||
card_url = f"{URL_BASE}/{card}"
|
||||
versioned_url = f"{card_url}?v={version}"
|
||||
|
||||
if card_url not in existing:
|
||||
# Card not registered yet — add it
|
||||
await resources.async_create_item(
|
||||
{"url": versioned_url, "res_type": "module"}
|
||||
)
|
||||
_LOGGER.info("TaskMate: added resource: %s", versioned_url)
|
||||
else:
|
||||
item = existing[card_url]
|
||||
current_url = item.get("url", "")
|
||||
if current_url != versioned_url:
|
||||
# Version string changed — update it
|
||||
await resources.async_update_item(
|
||||
item["id"],
|
||||
{"url": versioned_url, "res_type": "module"},
|
||||
)
|
||||
_LOGGER.info(
|
||||
"TaskMate: updated resource: %s -> %s",
|
||||
current_url, versioned_url,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("TaskMate: resource up to date: %s", versioned_url)
|
||||
|
||||
# Deregister retired cards by EXACT URL match only. This is the one
|
||||
# delete we allow: each target is a specific /taskmate/<file>.js that no
|
||||
# longer exists, looked up in the existing map we already built. No
|
||||
# diffing of "unexpected" URLs, so it cannot cascade into deleting live
|
||||
# resources the way the old blanket cleanup did.
|
||||
if hasattr(resources, "async_delete_item"):
|
||||
for retired in RETIRED_CARDS:
|
||||
item = existing.get(f"{URL_BASE}/{retired}")
|
||||
if item is None:
|
||||
continue
|
||||
try:
|
||||
await resources.async_delete_item(item["id"])
|
||||
_LOGGER.info(
|
||||
"TaskMate: removed retired resource: %s", item.get("url")
|
||||
)
|
||||
except (AttributeError, KeyError, TypeError, OSError) as err:
|
||||
_LOGGER.warning(
|
||||
"TaskMate: could not remove retired resource %s: %s",
|
||||
item.get("url"), err,
|
||||
)
|
||||
|
||||
except (AttributeError, KeyError, TypeError, OSError) as err:
|
||||
_LOGGER.error("TaskMate: error managing Lovelace resources: %s", err)
|
||||
Reference in New Issue
Block a user