From b92af0bdc758e4a8e19472396adba8cbe34a8470 Mon Sep 17 00:00:00 2001 From: Graham Hosking <142685548+ITSpecialist111@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:06:35 +0100 Subject: [PATCH 1/2] Inspect scripts alongside automations --- README.md | 10 ++- automation_inspector/DOCS.md | 8 +- automation_inspector/app/dependency_map.py | 91 +++++++++++++++++----- automation_inspector/app/ha_client.py | 54 +++++++++++-- automation_inspector/config.yaml | 4 +- automation_inspector/translations/en.yaml | 2 +- automation_inspector/www/app.js | 42 ++++++---- automation_inspector/www/index.html | 14 ++-- tests/test_dependency_map.py | 69 ++++++++++++++++ tests/test_ha_client.py | 58 +++++++++++--- 10 files changed, 284 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index a71b6b6..9841041 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Automation Inspector -Automation Inspector is a read-only **Home Assistant App** that audits every loaded automation and, when available, every UI-managed automation in `automations.yaml`. +Automation Inspector is a read-only **Home Assistant App** that audits every loaded automation and script and, when available, every UI-managed automation in `automations.yaml`. -It resolves modern Home Assistant targets, checks dependency health, validates automation syntax, identifies Home Assistant 2026.7 migrations, and surfaces recent trace failures. All analysis runs locally inside Home Assistant; there is no telemetry or external data service. +It resolves modern Home Assistant targets, checks dependency health, validates automation and script syntax, identifies Home Assistant 2026.7 migrations, and surfaces recent trace failures. All analysis runs locally inside Home Assistant; there is no telemetry or external data service. > [!IMPORTANT] > Version 1.0.0 is a breaking upgrade. It requires Home Assistant 2026.7.0 or newer, removes unauthenticated host-port access, and drops `armv7`. See [Migrating from 0.4.x](#migrating-from-04x). @@ -31,6 +31,7 @@ _Screenshots use synthetic demo data; no Home Assistant instance data is include ## Highlights - **Complete automation config** — reads each loaded automation through the canonical `automation/config` WebSocket API instead of relying on state attributes. +- **Script inspection** — reads loaded scripts through Home Assistant and checks their dependencies, actions, compatibility, and traces alongside automations. - **Current target model** — understands entity, device, area, floor, and label targets. - **Purpose-aware resolution** — filters resolved entities using the trigger, condition, or action target metadata Home Assistant itself publishes. - **Runtime-aware templates** — preserves templated target values as runtime-resolved metadata without reporting false missing entities. @@ -122,6 +123,7 @@ One authenticated WebSocket connection batches: - entity, device, area, floor, and label registries; - entity integration sources and service descriptions; - loaded automation configurations; +- loaded script configurations; - trigger and condition platform descriptions; - target extraction and config validation requests; - trace summaries and failed trace details. @@ -130,7 +132,7 @@ One authenticated WebSocket connection batches: ## Report semantics -An automation needs attention when one or more of these conditions apply: +An automation or script needs attention when one or more of these conditions apply: - an entity reference is missing, unavailable, unknown, or disabled; - a device, area, floor, or label target no longer exists; @@ -139,7 +141,7 @@ An automation needs attention when one or more of these conditions apply: - the latest stored run failed or contains template errors; - an entry exists in `automations.yaml` but did not load. -“Unreferenced helpers” are cleanup candidates, not deletion instructions. A helper can still be used by scripts, dashboards, templates, integrations, or external clients. +“Unreferenced helpers” are cleanup candidates, not deletion instructions. A helper can still be used by dashboards, templates, integrations, or external clients. ## HTTP API diff --git a/automation_inspector/DOCS.md b/automation_inspector/DOCS.md index 622a428..d855314 100644 --- a/automation_inspector/DOCS.md +++ b/automation_inspector/DOCS.md @@ -1,6 +1,6 @@ # Automation Inspector App documentation -Automation Inspector performs read-only health analysis of Home Assistant automations. Open it from the Home Assistant sidebar after starting the App. +Automation Inspector performs read-only health analysis of Home Assistant automations and scripts. Open it from the Home Assistant sidebar after starting the App. ## Configuration @@ -9,7 +9,7 @@ Automation Inspector performs read-only health analysis of Home Assistant automa | `refresh_interval` | 300 | Seconds between background inspections | | `request_timeout` | 15 | Timeout for each WebSocket response | | `include_disabled` | `true` | Analyze automations that are turned off | -| `inspect_traces` | `true` | Retrieve details for recent failed traces | +| `inspect_traces` | `true` | Retrieve details for recent failed automation and script traces | | `scan_automations_file` | `true` | Find UI-managed automations that did not load | Restart the App after changing options. @@ -21,7 +21,7 @@ Restart the App after changing options. - **Unavailable / unknown** — Home Assistant has a state object, but its current state is unhealthy. - **Not loaded** — the automation exists in `automations.yaml` but has no runtime automation entity. - **Unresolved target** — a referenced device, area, floor, or label no longer exists. -- **Runtime target** — a Jinja template such as `{{ sonos_speaker }}` that Home Assistant resolves only when the automation runs; this is informational, not a failure. +- **Runtime target** — a Jinja template such as `{{ sonos_speaker }}` that Home Assistant resolves only when the automation or script runs; this is informational, not a failure. - **Compatibility** — Home Assistant validation failed or the configuration uses a removed/deprecated construct. - **Trace failure** — the latest retained execution ended in an error or contains template errors. @@ -49,7 +49,7 @@ This is intentional in 1.0.0. Automation definitions and entity states are sensi ### Unreferenced helper appears in use elsewhere -The helper list considers automations only. Scripts, dashboards, integrations, templates, and external clients are outside its scope. Always verify before deleting a helper. +The helper list considers inspected automations and scripts only. Dashboards, integrations, templates, and external clients are outside its scope. Always verify before deleting a helper. ## Data handling diff --git a/automation_inspector/app/dependency_map.py b/automation_inspector/app/dependency_map.py index e97fa9e..55c0456 100644 --- a/automation_inspector/app/dependency_map.py +++ b/automation_inspector/app/dependency_map.py @@ -185,28 +185,32 @@ def _latest_traces(traces: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any] for trace in traces: if trace.get("not_triggered"): continue + domain = str(trace.get("domain") or "automation") item_id = trace.get("item_id") timestamp = trace.get("timestamp") if not isinstance(item_id, str) or not isinstance(timestamp, dict): continue + key = f"{domain}:{item_id}" start = str(timestamp.get("start", "")) - previous = latest.get(item_id) + previous = latest.get(key) previous_start = str((previous or {}).get("timestamp", {}).get("start", "")) if previous is None or start > previous_start: - latest[item_id] = trace + latest[key] = trace return latest def _trace_info( + domain: str, config_id: str | None, latest: Mapping[str, dict[str, Any]], details: Mapping[str, dict[str, Any]], ) -> dict[str, Any] | None: - if not config_id or config_id not in latest: + trace_key = f"{domain}:{config_id}" if config_id else None + if not trace_key or trace_key not in latest: return None - summary = latest[config_id] + summary = latest[trace_key] template_errors: list[str] = [] - detail = details.get(config_id) + detail = details.get(trace_key) if isinstance(detail, dict): steps = detail.get("trace") if isinstance(steps, dict): @@ -328,6 +332,7 @@ def _target_rows( def _analyze_automation( *, + domain: str, key: str, state: dict[str, Any] | None, configs: list[tuple[str, dict[str, Any]]], @@ -383,7 +388,7 @@ def _analyze_automation( } ) - trace = _trace_info(config_id, latest_traces, snapshot.trace_details) + trace = _trace_info(domain, config_id, latest_traces, snapshot.trace_details) trace_is_issue = bool( trace and ( @@ -396,11 +401,12 @@ def _analyze_automation( compatibility_errors = sum( 1 for finding in compatibility if finding["severity"] in {"error", "warning"} ) + state_value = str(state.get("state")) if state is not None else None if not loaded: status = "not_loaded" - elif state is not None and str(state.get("state")) == "on": + elif state_value == "on" or (domain == "script" and state_value != "unavailable"): status = "enabled" - elif state is not None and str(state.get("state")) == "unavailable": + elif state_value == "unavailable": status = "unavailable" else: status = "disabled" @@ -409,6 +415,8 @@ def _analyze_automation( warnings: list[str] = [] if loaded and key in snapshot.automation_config_errors: warnings.append(snapshot.automation_config_errors[key]) + if loaded and domain == "script" and key in snapshot.script_config_errors: + warnings.append(snapshot.script_config_errors[key]) if "use_blueprint" in primary_config: warnings.append("Blueprint analysis is limited to its configured inputs.") @@ -417,6 +425,8 @@ def _analyze_automation( ) return { "entity_id": key if loaded else None, + "domain": domain, + "item_type": "automation" if domain == "automation" else "script", "friendly_name": friendly_name, "enabled": status == "enabled", "loaded": loaded, @@ -482,6 +492,7 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, latest_traces = _latest_traces(snapshot.traces) automations: dict[str, dict[str, Any]] = {} + scripts: dict[str, dict[str, Any]] = {} matched_file_keys: set[str] = set() for entity_id, state in sorted(state_map.items()): if not entity_id.startswith("automation."): @@ -508,6 +519,7 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, if not configs: configs.append((f"attributes:{entity_id}", attributes)) automations[entity_id] = _analyze_automation( + domain="automation", key=entity_id, state=state, configs=configs, @@ -520,6 +532,35 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, latest_traces=latest_traces, ) + for entity_id, state in sorted(state_map.items()): + if not entity_id.startswith("script."): + continue + attributes = state.get("attributes", {}) + if not isinstance(attributes, dict): + attributes = {} + raw_config_id = attributes.get("id") or entity_id.split(".", 1)[1] + config_id = str(raw_config_id) if raw_config_id is not None else None + runtime_config = snapshot.script_configs.get(entity_id) + + script_configs: list[tuple[str, dict[str, Any]]] = [] + if runtime_config: + script_configs.append((f"runtime:{entity_id}", runtime_config)) + else: + script_configs.append((f"attributes:{entity_id}", attributes)) + scripts[entity_id] = _analyze_automation( + domain="script", + key=entity_id, + state=state, + configs=script_configs, + config_id=config_id, + loaded=True, + snapshot=snapshot, + state_map=state_map, + registry_map=registry_map, + known_domains=known_domains, + latest_traces=latest_traces, + ) + for file_automation in snapshot.file_automations: if file_automation.key in matched_file_keys: continue @@ -527,6 +568,7 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, if key in automations: key = f"{key}:{file_automation.index}" automations[key] = _analyze_automation( + domain="automation", key=key, state=None, configs=[(file_automation.key, file_automation.config)], @@ -540,7 +582,9 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, ) all_referenced = { - entity["id"] for automation in automations.values() for entity in automation["entities"] + entity["id"] + for item in [*automations.values(), *scripts.values()] + for entity in item["entities"] } unreferenced_helpers = [] for entity_id in sorted(set(state_map) | set(registry_map)): @@ -567,18 +611,18 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, ) entity_rows = [ - entity for automation in automations.values() for entity in automation["entities"] + entity for item in [*automations.values(), *scripts.values()] for entity in item["entities"] ] compatibility_rows = [ finding - for automation in automations.values() - for finding in automation["compatibility_issues"] + for item in [*automations.values(), *scripts.values()] + for finding in item["compatibility_issues"] if finding["severity"] in {"error", "warning"} ] unresolved_targets = sum( len(target[field]) - for automation in automations.values() - for target in automation["targets"] + for item in [*automations.values(), *scripts.values()] + for target in item["targets"] for field in ( "missing_devices", "missing_areas", @@ -588,12 +632,12 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, ) trace_failures = sum( 1 - for automation in automations.values() - if automation["trace"] + for item in [*automations.values(), *scripts.values()] + if item["trace"] and ( - automation["trace"].get("error") - or automation["trace"].get("template_errors") - or automation["trace"].get("script_execution") in {"error", "failed_max_runs"} + item["trace"].get("error") + or item["trace"].get("template_errors") + or item["trace"].get("script_execution") in {"error", "failed_max_runs"} ) ) ha_config = snapshot.home_assistant_config @@ -608,8 +652,12 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, }, "summary": { "automations": len(automations), + "scripts": len(scripts), + "inspected_items": len(automations) + len(scripts), "enabled": sum(1 for item in automations.values() if item["enabled"]), + "enabled_scripts": sum(1 for item in scripts.values() if item["enabled"]), "disabled": sum(1 for item in automations.values() if item["status"] == "disabled"), + "disabled_scripts": sum(1 for item in scripts.values() if item["status"] == "disabled"), "unloaded": sum(1 for item in automations.values() if not item["loaded"]), "dependency_references": len(entity_rows), "unique_entities": len({row["id"] for row in entity_rows}), @@ -620,6 +668,10 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, "automations_with_issues": sum( 1 for item in automations.values() if item["issue_count"] > 0 ), + "scripts_with_issues": sum(1 for item in scripts.values() if item["issue_count"] > 0), + "items_with_issues": sum( + 1 for item in [*automations.values(), *scripts.values()] if item["issue_count"] > 0 + ), "compatibility_issues": len(compatibility_rows), "unresolved_targets": unresolved_targets, "trace_failures": trace_failures, @@ -627,6 +679,7 @@ def build_inspection(snapshot: SourceSnapshot, settings: Settings) -> dict[str, "duration_ms": round((time.perf_counter() - started) * 1000, 1), }, "automations": automations, + "scripts": scripts, "unreferenced_helpers": unreferenced_helpers, "orphans": [helper["id"] for helper in unreferenced_helpers], "warnings": snapshot.warnings, diff --git a/automation_inspector/app/ha_client.py b/automation_inspector/app/ha_client.py index 75f8df9..ee39db1 100644 --- a/automation_inspector/app/ha_client.py +++ b/automation_inspector/app/ha_client.py @@ -54,6 +54,8 @@ class SourceSnapshot: home_assistant_config: dict[str, Any] automation_configs: dict[str, dict[str, Any]] = field(default_factory=dict) automation_config_errors: dict[str, str] = field(default_factory=dict) + script_configs: dict[str, dict[str, Any]] = field(default_factory=dict) + script_config_errors: dict[str, str] = field(default_factory=dict) file_automations: list[FileAutomation] = field(default_factory=list) entity_registry: list[dict[str, Any]] = field(default_factory=list) device_registry: list[dict[str, Any]] = field(default_factory=list) @@ -206,15 +208,17 @@ def _latest_traces(traces: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: for trace in traces: if trace.get("not_triggered"): continue + domain = str(trace.get("domain") or "automation") item_id = trace.get("item_id") timestamp = trace.get("timestamp") if not isinstance(item_id, str) or not isinstance(timestamp, dict): continue + key = f"{domain}:{item_id}" start = str(timestamp.get("start", "")) - previous = latest.get(item_id) + previous = latest.get(key) previous_start = str((previous or {}).get("timestamp", {}).get("start", "")) if previous is None or start > previous_start: - latest[item_id] = trace + latest[key] = trace return latest @@ -280,7 +284,8 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna "label_registry": {"type": "config/label_registry/list"}, "entity_sources": {"type": "entity/source"}, "services": {"type": "get_services"}, - "traces": {"type": "trace/list", "domain": "automation"}, + "automation_traces": {"type": "trace/list", "domain": "automation"}, + "script_traces": {"type": "trace/list", "domain": "script"}, } ) @@ -301,6 +306,11 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna for item in states if str(item.get("entity_id", "")).startswith("automation.") ) + script_ids = sorted( + str(item["entity_id"]) + for item in states + if str(item.get("entity_id", "")).startswith("script.") + ) config_results = await _call_in_batches( session, { @@ -308,6 +318,13 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna for entity_id in automation_ids }, ) + script_config_results = await _call_in_batches( + session, + { + entity_id: {"type": "script/config", "entity_id": entity_id} + for entity_id in script_ids + }, + ) automation_configs: dict[str, dict[str, Any]] = {} automation_config_errors: dict[str, str] = {} for entity_id, response in config_results.items(): @@ -317,6 +334,15 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna automation_configs[entity_id] = config else: automation_config_errors[entity_id] = response.error or "Config unavailable" + script_configs: dict[str, dict[str, Any]] = {} + script_config_errors: dict[str, str] = {} + for entity_id, response in script_config_results.items(): + result = response.result + config = result.get("config") if isinstance(result, dict) else None + if response.success and isinstance(config, dict): + script_configs[entity_id] = config + else: + script_config_errors[entity_id] = response.error or "Config unavailable" trigger_snapshot = await session.subscription_snapshot("trigger_platforms/subscribe") condition_snapshot = await session.subscription_snapshot( @@ -340,6 +366,9 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna configs_for_analysis: dict[str, dict[str, Any]] = { f"runtime:{entity_id}": config for entity_id, config in automation_configs.items() } + configs_for_analysis.update( + {f"runtime:{entity_id}": config for entity_id, config in script_configs.items()} + ) configs_for_analysis.update( {automation.key: automation.config for automation in file_automations} ) @@ -369,10 +398,17 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna if request: detail_requests[f"validation:{key}"] = request - traces = _list_result(base_results, "traces", warnings) + traces = [ + dict(trace, domain="automation") + for trace in _list_result(base_results, "automation_traces", warnings) + ] + traces.extend( + dict(trace, domain="script") + for trace in _list_result(base_results, "script_traces", warnings) + ) latest_traces = _latest_traces(traces) if self.settings.inspect_traces: - for item_id, trace in latest_traces.items(): + for trace_key, trace in latest_traces.items(): if trace.get("script_execution") not in { "error", "aborted", @@ -380,10 +416,12 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna } and not trace.get("error"): continue run_id = trace.get("run_id") + domain = str(trace.get("domain") or "automation") + item_id = trace.get("item_id") if isinstance(run_id, str): - detail_requests[f"trace:{item_id}"] = { + detail_requests[f"trace:{trace_key}"] = { "type": "trace/get", - "domain": "automation", + "domain": domain, "item_id": item_id, "run_id": run_id, } @@ -422,6 +460,8 @@ async def _fetch_once(self, file_automations: list[FileAutomation]) -> SourceSna home_assistant_config=config_response.result, automation_configs=automation_configs, automation_config_errors=automation_config_errors, + script_configs=script_configs, + script_config_errors=script_config_errors, file_automations=file_automations, entity_registry=_list_result(base_results, "entity_registry", warnings), device_registry=_list_result(base_results, "device_registry", warnings), diff --git a/automation_inspector/config.yaml b/automation_inspector/config.yaml index c039a1b..151cfe7 100644 --- a/automation_inspector/config.yaml +++ b/automation_inspector/config.yaml @@ -2,8 +2,8 @@ name: Automation Inspector version: 1.0.2 slug: automation_inspector description: >- - Audits automation dependencies, targets, compatibility, and recent failures - without sending Home Assistant data outside your instance. + Audits automation and script dependencies, targets, compatibility, and recent + failures without sending Home Assistant data outside your instance. url: https://github.com/ITSpecialist111/Automation-Inspector arch: - aarch64 diff --git a/automation_inspector/translations/en.yaml b/automation_inspector/translations/en.yaml index db189f1..7ea814c 100644 --- a/automation_inspector/translations/en.yaml +++ b/automation_inspector/translations/en.yaml @@ -10,7 +10,7 @@ configuration: description: Analyze automations that are currently turned off as well as enabled automations. inspect_traces: name: Inspect recent traces - description: Retrieve details for recent failed automation runs and template errors. + description: Retrieve details for recent failed automation and script runs and template errors. scan_automations_file: name: Scan automations.yaml description: Read the configuration file to report automations that failed to load. \ No newline at end of file diff --git a/automation_inspector/www/app.js b/automation_inspector/www/app.js index 6eeda5c..cfb5836 100644 --- a/automation_inspector/www/app.js +++ b/automation_inspector/www/app.js @@ -185,6 +185,8 @@ function renderMetrics() { const metrics = element("metrics"); metrics.replaceChildren(); const summary = state.data?.summary || {}; + const itemCount = Number(summary.inspected_items || summary.automations || 0); + const scriptCount = Number(summary.scripts || 0); const unhealthyEntities = Number(summary.missing_entities || 0) + Number(summary.unavailable_entities || 0) + @@ -192,15 +194,15 @@ function renderMetrics() { Number(summary.disabled_entities || 0); metrics.append( metricCard( - "Automations", - summary.automations, - `${summary.enabled || 0} enabled · ${summary.disabled || 0} disabled`, + "Inspected items", + itemCount, + `${summary.automations || 0} automations · ${scriptCount} scripts`, ), metricCard( "Need attention", - summary.automations_with_issues, + summary.items_with_issues ?? summary.automations_with_issues, `${summary.unloaded || 0} not loaded`, - summary.automations_with_issues ? "danger" : "", + (summary.items_with_issues ?? summary.automations_with_issues) ? "danger" : "", ), metricCard( "Dependency health", @@ -280,7 +282,7 @@ function updateConnection(error = null) { } function automationSearchText(key, info) { - const parts = [key, info.friendly_name, info.status, info.config_id, info.mode]; + const parts = [key, info.friendly_name, info.status, info.config_id, info.mode, info.item_type]; (info.entities || []).forEach((entity) => { parts.push(entity.id, entity.name, entity.state, entity.status); }); @@ -301,6 +303,13 @@ function automationSearchText(key, info) { return parts.filter(Boolean).join(" ").toLocaleLowerCase(); } +function inspectionItems() { + return [ + ...Object.entries(state.data?.automations || {}), + ...Object.entries(state.data?.scripts || {}), + ]; +} + function daysSince(value) { const date = safeDate(value); return date ? (Date.now() - date.getTime()) / 86400000 : Number.POSITIVE_INFINITY; @@ -312,7 +321,7 @@ function filteredAutomations() { const run = element("run-filter").value; const issuesOnly = element("issues-only").checked; const sort = element("sort-filter").value; - const entries = Object.entries(state.data?.automations || {}).filter(([key, info]) => { + const entries = inspectionItems().filter(([key, info]) => { if (status !== "all" && info.status !== status) return false; if (issuesOnly && Number(info.issue_count || 0) === 0) return false; const age = daysSince(info.last_triggered); @@ -354,13 +363,17 @@ function entityLink(entity, preview = false) { } function automationHeader(key, info) { + const itemType = info.item_type === "script" ? "Script" : "Automation"; const main = create("div", { className: "automation-main" }); const heading = create("div", { className: "automation-heading" }); heading.append(statusBadge(info.status)); const title = create("div", { className: "automation-title-wrap" }); title.append( create("h3", { className: "automation-title", text: info.friendly_name || key }), - create("span", { className: "automation-id", text: info.entity_id || `YAML · ${info.config_id || key}` }), + create("span", { + className: "automation-id", + text: `${itemType} · ${info.entity_id || `YAML · ${info.config_id || key}`}`, + }), ); heading.append(title); @@ -398,15 +411,16 @@ function automationHeader(key, info) { const actions = create("div", { className: "automation-actions" }); if (info.loaded && info.config_id) { + const domain = info.item_type === "script" ? "script" : "automation"; actions.append( link( "Edit", - homeAssistantUrl(`/config/automation/edit/${encodeURIComponent(info.config_id)}`), + homeAssistantUrl(`/config/${domain}/edit/${encodeURIComponent(info.config_id)}`), "button button-secondary small-button", ), link( "Traces", - homeAssistantUrl(`/config/automation/trace/${encodeURIComponent(info.config_id)}`), + homeAssistantUrl(`/config/${domain}/trace/${encodeURIComponent(info.config_id)}`), "button button-ghost small-button", ), ); @@ -611,10 +625,11 @@ function renderAutomations() { const loadMoreRow = element("load-more-row"); loadMoreRow.classList.toggle("hidden", visible.length >= entries.length); element("load-more").textContent = `Show ${Math.min(PAGE_SIZE, entries.length - visible.length)} more`; + const total = inspectionItems().length; element("result-count").textContent = - entries.length === Object.keys(state.data.automations || {}).length - ? `${entries.length} automations inspected` - : `${entries.length} of ${Object.keys(state.data.automations || {}).length} automations match`; + entries.length === total + ? `${entries.length} items inspected` + : `${entries.length} of ${total} items match`; } function renderHelpers() { @@ -698,6 +713,7 @@ async function loadInspection(force = false) { if (!body || body.schema_version !== 2 || typeof body.automations !== "object") { throw new Error("The server returned an unsupported inspection format."); } + if (!body.scripts || typeof body.scripts !== "object") body.scripts = {}; state.data = body; state.etag = response.headers.get("ETag"); state.lastLoadedAt = new Date(); diff --git a/automation_inspector/www/index.html b/automation_inspector/www/index.html index a915825..7eb3ee9 100644 --- a/automation_inspector/www/index.html +++ b/automation_inspector/www/index.html @@ -4,7 +4,7 @@ - + Automation Inspector