diff --git a/README.md b/README.md index 9c29d69e..dbde29a2 100755 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Live, personal Home Assistant configuration shared for **browsing and inspiratio - You are here: `/` (root repo guide) - [Blog](https://www.vcloudinfo.com) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) | [Diagram](config/www/custom_ui/floorplan/images/branding/Bear-Stone-Docker-Diagram.jpg) | [YouTube](https://youtube.com/vCloudInfo) - Config readmes: [Config index](config/README.md) | [Packages](config/packages/README.md) | [Automations](config/automation/README.md) | [Scripts](config/script/README.md) | [Scenes](config/scene/README.md) | [Sounds](config/sounds/README.md) | [Package triggers](config/packages/triggers/README.md) +- Codex skills (optional): [codex_skills](codex_skills) ![Home Assistant header](https://i.imgur.com/vjDH1LJ.png) diff --git a/codex_skills/README.md b/codex_skills/README.md new file mode 100644 index 00000000..11280fd7 --- /dev/null +++ b/codex_skills/README.md @@ -0,0 +1,20 @@ +

+ Codex Skills +

+ +Optional Codex skills stored in-repo so they can be shared with the community. + +### Quick navigation +- You are here: `codex_skills/` +- [Repo overview](../README.md) | [Dashboards](../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) + +## Skills + +- `homeassistant-dashboard-designer/` + - A constrained, button-card-first Home Assistant Lovelace dashboard design system + a small YAML lint helper. + +### Notes +- These skills are documentation + helper scripts only; they should not contain tokens, secrets, or environment-specific credentials. +- Codex loads skills from your local Codex home. See each skill's README for install steps. + + diff --git a/codex_skills/homeassistant-dashboard-designer/README.md b/codex_skills/homeassistant-dashboard-designer/README.md new file mode 100644 index 00000000..21c370dc --- /dev/null +++ b/codex_skills/homeassistant-dashboard-designer/README.md @@ -0,0 +1,57 @@ +

+ Home Assistant Dashboard Designer (Codex Skill) +

+ +This directory contains the `homeassistant-dashboard-designer` Codex skill, stored in-repo so it can be shared with the community. + +### Quick navigation +- You are here: `codex_skills/homeassistant-dashboard-designer/` +- [Repo overview](../../README.md) | [Codex skills](../README.md) | [Dashboards](../../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) + +## What This Skill Does + +- Enforces a constrained Lovelace design system (button-card first, minimal card-mod, grid/vertical-stack layout rules). +- Encourages centralized templates and deterministic YAML output. +- Treats Stitch MCP output as *inspiration only* and translates ideas into safe Lovelace YAML. + +Skill source of truth inside this folder: +- `SKILL.md` +- `agents/openai.yaml` +- `references/dashboard_rules.md` +- `scripts/validate_lovelace_view.py` + +## Install (Local Codex) + +Codex loads skills from your local Codex skills directory. + +1. Copy this folder into your Codex skills directory as: + - `~/.codex/skills/homeassistant-dashboard-designer/` (Linux/macOS) + - `%USERPROFILE%\\.codex\\skills\\homeassistant-dashboard-designer\\` (Windows) +2. Restart your Codex session/editor so it re-indexes skills. + +## Stitch MCP Install (Design Inspiration) + +Stitch is optional and used only for design inspiration. To enable it, add a Stitch MCP server entry to your Codex config. + +1. Set an environment variable with your API key: + - `STITCH_API_KEY=` +2. Add this to your `~/.codex/config.toml`: + +```toml +[mcp_servers.stitch] +url = "https://stitch.googleapis.com/mcp" +env_http_headers = { "X-Goog-Api-Key" = "STITCH_API_KEY" } +http_headers = { "Accept" = "application/json" } +``` + +## Usage + +Invoke in chat: +- `$homeassistant-dashboard-designer` + +Then provide the structured intent block described in `SKILL.md` (dashboard intent, view name, entity map, and layout constraints). + +### Notes +- This skill intentionally contains no secrets. Configure MCP credentials via environment variables in your local Codex setup. + + diff --git a/codex_skills/homeassistant-dashboard-designer/SKILL.md b/codex_skills/homeassistant-dashboard-designer/SKILL.md new file mode 100644 index 00000000..f719ce47 --- /dev/null +++ b/codex_skills/homeassistant-dashboard-designer/SKILL.md @@ -0,0 +1,143 @@ +--- +name: homeassistant-dashboard-designer +description: "Design, update, and refactor Home Assistant Lovelace dashboards (YAML views/partials) with a constrained, machine-safe design system: button-card-first structure, minimal card-mod styling, optional flex-horseshoe + mini-graph telemetry, strict grid/vertical-stack layout rules, centralized templates, deterministic ordering, and config validation. Use for Home Assistant dashboard work (especially config/dashboards/**), when refactoring views, adding infra/home/energy/environment panels, or translating Stitch design inspiration into safe Lovelace YAML." +--- + +# Home Assistant Dashboard Designer + +Design dashboards as systems: predictable structure, reusable templates, minimal drift. Treat Stitch as inspiration only; translate to safe Lovelace YAML. + +## Input Contract (Ask For This If Missing) + +Request structured intent, not raw YAML: + +```yaml +dashboard_type: infra # infra|home|energy|environment +view_name: "Infrastructure Overview" +target_paths: + dashboard_yaml: "config/dashboards/infrastructure/dashboard.yaml" + view_yaml: "config/dashboards/infrastructure/views/07_docker_containers.yaml" +entities: + - name: "Proxmox01" + cpu: "sensor.proxmox01_cpu" + memory: "sensor.proxmox01_mem" +constraints: + desktop_columns: 4 + mobile_columns: 2 +notes: + - "Preserve existing templates; minimize diff." +``` + +If updating an existing view, also ask: + +- Which view file (single view dict) is the source of truth? +- Any "don't touch" areas/templates? +- Any fallback cards already in use that must remain? + +## Output Contract (Always Produce) + +- One valid Lovelace view (single view dict) +- Deterministic ordering (stable ordering of cards/sections) +- Section comments (short, consistent) +- Zero per-card styling variance (no one-off styles; use templates/snippets) +- Fallback usage explicitly justified (YAML comments adjacent to the fallback card) + +## Card Priority Model + +Attempt Tier 1 first. Use Tier 2 only if Tier 1 cannot express the requirement safely. + +Tier 1 (primary): +- `custom:button-card` (structure + tiles + headers + containers) +- `card-mod` (shared/global polish only; shared snippets OK) +- `custom:flex-horseshoe-card` (single-metric radial telemetry) +- `custom:mini-graph-card` (single-metric trends) + +Tier 2 (fallback, must justify in comments): +- `entities`, `markdown`, `history-graph`, `conditional` +- `iframe` (last resort) + +## Layout Rules (Hard Constraints) + +- Allowed layout containers: `grid`, `vertical-stack` +- Maximum nesting depth: 2 (example: `grid` -> `vertical-stack` -> cards) +- No `horizontal-stack` inside grid cells (prefer: more grid columns, or vertical-stack) +- No freeform positioning +- No layout logic embedded in `card-mod` + +Note: If the repo/view uses Home Assistant `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section (sections should contain only `grid`/`vertical-stack` cards and their children). + +## Design System Rules (Implementation Discipline) + +### Button Card (Primary Renderer) + +- Use `custom:button-card` as the structural primitive. +- Templates required. Do not inline per-card `styles:`; keep styling inside centralized templates/snippets. +- Logic via variables or template JS only (avoid duplicating logic per-card). + +### card-mod (Styling Constraint Layer) + +- Allowed only for shared/global polish: + - padding normalization + - border radius control + - shadow suppression + - typography consistency +- No entity-specific styling +- No per-card visual experimentation + +### Flex Horseshoe Card + +- One primary metric per card. +- Explicit color thresholds. +- No mixed units. + +### Mini Graph Card + +- One metric per graph. +- Consistent time windows per dashboard. +- Minimal legends (only when required). + +## Template Architecture (Required) + +- Views reference templates only (for `custom:button-card`: every instance must specify `template:`). +- Templates accept variables; views pass variables. +- Do not create new templates unless explicitly instructed. + +Repo reality check (important): +- If the repo already has established templates (for example, Infrastructure uses `bearstone_*`), use what exists. +- If the spec's "required template categories" are not present, map to the closest existing template and call out the gap, or ask for explicit permission to add templates. + +## Stitch MCP (Inspiration Only) + +Use Stitch only to learn: +- visual hierarchy (headers, grouping, density) +- spacing rhythm (padding/gaps) +- grouping patterns (panels, sections) + +Never treat Stitch output as implementation code. + +When prompting Stitch, include these constraints verbatim: + +``` +Grid-only layout. No absolute positioning. No JS frameworks. No custom HTML/CSS outside card-mod. +Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, custom:mini-graph-card, and HA core fallback cards only if unavoidable. +Max layout nesting depth: 2. No horizontal-stack inside grid cells. +``` + +## Workflow (Do This In Order) + +1. Read the target dashboard/view/partials/templates to understand existing patterns and avoid drift. +2. Determine intent: `infra` (NOC), `home`, `energy`, or `environment`. Keep one intent per view. +3. Validate entities and services before editing (prefer Home Assistant MCP live context; otherwise ask user to confirm entity IDs). +4. Draft layout with constraints: a top-level `grid` and optional `vertical-stack` groups. +5. Implement using Tier 1 cards first; reuse existing templates; avoid one-off styles. +6. If fallback cards are necessary, add an inline comment explaining why Tier 1 cannot satisfy the requirement. +7. Validate: + - Run Home Assistant config validation (`ha core check` or `homeassistant --script check_config`) when available. + - Optionally run `scripts/validate_lovelace_view.py` from this skill against the changed view file to catch violations early. +8. Failure behavior: + - If requirements can't be met: state the violated rule and propose a compliant alternative. + - If validation fails: stop, surface the error output, and propose corrected YAML. Do not leave invalid config applied. + +## References + +- Read `references/dashboard_rules.md` when you need the full constraint set and repo-specific mapping notes. diff --git a/codex_skills/homeassistant-dashboard-designer/agents/openai.yaml b/codex_skills/homeassistant-dashboard-designer/agents/openai.yaml new file mode 100644 index 00000000..9849e4ba --- /dev/null +++ b/codex_skills/homeassistant-dashboard-designer/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Home Assistant Dashboard Designer" + short_description: "Design/refactor HA Lovelace dashboards safely" + default_prompt: "Use $homeassistant-dashboard-designer to design, update, or refactor a Home Assistant Lovelace dashboard view using button-card templates + safe grid/stack layouts." diff --git a/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md b/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md new file mode 100644 index 00000000..6173e4e4 --- /dev/null +++ b/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md @@ -0,0 +1,119 @@ +# Dashboard Rules (Full) + +This file is reference material for $homeassistant-dashboard-designer. Prefer following the workflow in `../SKILL.md`, and read this only when you need details. + +## Card Priority Model + +Tier 1 (primary, attempt first): +- `custom:button-card` +- `card-mod` (shared/global styling only) +- `custom:flex-horseshoe-card` +- `custom:mini-graph-card` + +Tier 2 (fallback, only when Tier 1 cannot satisfy the requirement; justify inline): +- `entities` +- `markdown` +- `history-graph` +- `conditional` +- `iframe` (last resort) + +## Layout Rules (Hard Constraints) + +- Allowed layout containers: `grid`, `vertical-stack` +- Maximum nesting depth: 2 +- No `horizontal-stack` inside grid cells +- No freeform positioning +- No layout logic embedded in `card-mod` + +Sections-mode note: +- If a view uses `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section. + +## Template Architecture (Required) + +Intent: centralize visuals and logic into templates; views pass only variables and remain uniform. + +Rules: +- `custom:button-card` must use templates (no one-off per-card styles). +- Templates accept variables. +- Do not hardcode entity IDs inside templates. +- Do not create new templates without explicit instruction. + +Reality note for CCOSTAN/Home-AssistantConfig: +- Infrastructure templates already exist in: + - `config/dashboards/infrastructure/templates/button_card_templates.yaml` +- Existing template naming uses `bearstone_*` and includes styling inside templates (this is OK). +- When asked to implement new conceptual categories (e.g., "status_pill"), first map to existing templates (e.g., `bearstone_infra_chip*`). If no suitable match exists, ask before adding templates. + +## Design System (Discipline) + +### Button Card + +Use it for: +- Infrastructure tiles +- Status pills +- Section headers +- Control footers +- Card containers + +Rules: +- Templates required. +- Avoid per-card `styles:` keys. +- Avoid per-card `card_mod:` blocks. + +### card-mod + +Allowed only for shared/global polish: +- padding normalization +- border radius control +- shadow suppression +- typography consistency + +Not allowed: +- entity-specific styling +- per-card visual experimentation + +### Flex Horseshoe Card + +Use it for: +- Temperature +- Battery +- Load +- Utilization +- Percentage-based metrics + +Rules: +- One primary metric per card. +- Explicit color thresholds. +- No mixed units. + +### Mini Graph Card + +Rules: +- One metric per graph. +- Consistent time windows per dashboard (keep uniform unless user asks otherwise). +- Minimal legends (only if required for interpretation). + +## Stitch MCP Integration (Inspiration Only) + +Stitch is advisory, never authoritative. + +When using Stitch: +- Provide feasibility constraints (cards + layout) +- Use outputs only to inform hierarchy/grouping/density/spacing +- Translate into approved Lovelace YAML + +Required constraints to include in Stitch prompt: + +``` +Grid-only layout. No absolute positioning. No JS frameworks. No custom HTML/CSS outside card-mod. +Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, custom:mini-graph-card, and HA core fallback cards only if unavoidable. +Max layout nesting depth: 2. No horizontal-stack inside grid cells. +``` + +## Repo-Specific Dashboard YAML Rules (config/dashboards/**) + +If working in this repo's `config/dashboards/` tree: +- Do not edit `config/.storage` (runtime state). +- Includes must use absolute container paths starting with `/config/`. +- Views are one file per view, and the dashboard file uses `!include_dir_list`. +- Files under `config/dashboards/**/*.yaml` must include the standard `@CCOSTAN` header block. diff --git a/codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py b/codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py new file mode 100644 index 00000000..ab42f33d --- /dev/null +++ b/codex_skills/homeassistant-dashboard-designer/scripts/validate_lovelace_view.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python +""" +Lightweight validator for Lovelace view YAML files. + +Goal: catch common violations of the homeassistant-dashboard-designer constraints +before Home Assistant reloads. + +This is NOT a full Lovelace schema validator. +""" + +from __future__ import annotations + +import argparse +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import yaml + + +class _Tagged(str): + pass + + +class _Loader(yaml.SafeLoader): + pass + + +def _construct_undefined(loader: _Loader, node: yaml.Node) -> Any: + # Preserve unknown tags (e.g., !include, !include_dir_list) as opaque strings. + if isinstance(node, yaml.ScalarNode): + return _Tagged(f"{node.tag} {node.value}") + if isinstance(node, yaml.SequenceNode): + seq = loader.construct_sequence(node) + return _Tagged(f"{node.tag} {seq!r}") + if isinstance(node, yaml.MappingNode): + mapping = loader.construct_mapping(node) + return _Tagged(f"{node.tag} {mapping!r}") + return _Tagged(f"{node.tag}") + + +_Loader.add_constructor(None, _construct_undefined) # type: ignore[arg-type] + + +@dataclass(frozen=True) +class Finding: + level: str # "ERROR" | "WARN" + path: str + message: str + + +ALLOWED_LAYOUT = {"grid", "vertical-stack"} +DISALLOWED_CARD_TYPES = {"horizontal-stack"} + +# Cards allowed without justification (design-system tiering is enforced in SKILL.md; this is a lint). +ALLOWED_CARD_TYPES = { + "grid", + "vertical-stack", + "custom:button-card", + "custom:flex-horseshoe-card", + "custom:mini-graph-card", + # Tier-2 fallbacks (validator does not enforce justification comments). + "entities", + "markdown", + "history-graph", + "conditional", + "iframe", +} + + +def _is_mapping(v: Any) -> bool: + return isinstance(v, dict) + + +def _is_sequence(v: Any) -> bool: + return isinstance(v, list) + + +def _card_type(card: dict[str, Any]) -> str | None: + t = card.get("type") + if isinstance(t, str): + return t + return None + + +def _validate_layout_depth( + *, + node: Any, + cur_depth: int, + max_depth: int, + node_path: str, + findings: list[Finding], +) -> None: + if not _is_mapping(node): + return + + ctype = _card_type(node) + if ctype in ALLOWED_LAYOUT: + cur_depth += 1 + if cur_depth > max_depth: + findings.append( + Finding( + level="ERROR", + path=node_path, + message=f"Layout nesting depth {cur_depth} exceeds max {max_depth}.", + ) + ) + + cards = node.get("cards") + if _is_sequence(cards): + for idx, child in enumerate(cards): + _validate_layout_depth( + node=child, + cur_depth=cur_depth, + max_depth=max_depth, + node_path=f"{node_path}.cards[{idx}]", + findings=findings, + ) + + +def _walk_cards(node: Any, node_path: str, findings: list[Finding]) -> None: + if _is_mapping(node): + ctype = _card_type(node) + if ctype in DISALLOWED_CARD_TYPES: + findings.append( + Finding( + level="ERROR", + path=node_path, + message=f"Disallowed card type: {ctype}", + ) + ) + if ctype and ctype not in ALLOWED_CARD_TYPES: + findings.append( + Finding( + level="WARN", + path=node_path, + message=f"Unknown/unlisted card type: {ctype}", + ) + ) + + if ctype == "custom:button-card": + tmpl = node.get("template") + if tmpl is None: + findings.append( + Finding( + level="ERROR", + path=node_path, + message="custom:button-card must set template: (string or list).", + ) + ) + if "styles" in node: + findings.append( + Finding( + level="ERROR", + path=node_path, + message="Per-card styles: are not allowed; move styling into centralized templates.", + ) + ) + if "card_mod" in node: + findings.append( + Finding( + level="ERROR", + path=node_path, + message="Per-card card_mod: is not allowed on button-card instances; use shared snippets or templates.", + ) + ) + + cards = node.get("cards") + if _is_sequence(cards): + for idx, child in enumerate(cards): + _walk_cards(child, f"{node_path}.cards[{idx}]", findings) + + elif _is_sequence(node): + for idx, child in enumerate(node): + _walk_cards(child, f"{node_path}[{idx}]", findings) + + +def _load_yaml(path: Path) -> Any: + txt = path.read_text(encoding="utf-8") + return yaml.load(txt, Loader=_Loader) + + +def main(argv: list[str]) -> int: + ap = argparse.ArgumentParser() + ap.add_argument("view_yaml", type=Path, help="Path to a Lovelace view YAML file") + ap.add_argument("--max-layout-depth", type=int, default=2) + ap.add_argument("--strict", action="store_true", help="Treat WARN findings as errors (non-zero exit).") + args = ap.parse_args(argv) + + if not args.view_yaml.exists(): + print(f"ERROR: file not found: {args.view_yaml}", file=sys.stderr) + return 2 + + try: + doc = _load_yaml(args.view_yaml) + except Exception as e: + print(f"ERROR: failed to parse YAML: {e}", file=sys.stderr) + return 2 + + findings: list[Finding] = [] + + # The repo uses "one YAML file per view (single view dict)". + if not _is_mapping(doc): + findings.append(Finding(level="ERROR", path="$", message="View file must be a single YAML mapping (one view dict).")) + else: + # Support both classic views (`cards:`) and sections views (`type: sections`, `sections:`). + if "cards" in doc: + _validate_layout_depth( + node=doc, + cur_depth=0, + max_depth=args.max_layout_depth, + node_path="$", + findings=findings, + ) + _walk_cards(doc.get("cards", []), "$.cards", findings) + elif doc.get("type") == "sections" and "sections" in doc: + sections = doc.get("sections") + if isinstance(sections, _Tagged): + findings.append( + Finding( + level="WARN", + path="$.sections", + message="sections is an !include/unknown tag; validator cannot inspect included sections content.", + ) + ) + elif _is_sequence(sections): + for sidx, section in enumerate(sections): + spath = f"$.sections[{sidx}]" + if not _is_mapping(section): + findings.append(Finding(level="ERROR", path=spath, message="Section must be a mapping.")) + continue + + _validate_layout_depth( + node=section, + cur_depth=0, + max_depth=args.max_layout_depth, + node_path=spath, + findings=findings, + ) + _walk_cards(section.get("cards", []), f"{spath}.cards", findings) + else: + findings.append( + Finding( + level="ERROR", + path="$.sections", + message="sections must be a list (or an !include tag).", + ) + ) + else: + findings.append( + Finding( + level="ERROR", + path="$", + message="View dict must contain cards: or be type: sections with sections:.", + ) + ) + + errors = [f for f in findings if f.level == "ERROR"] + warns = [f for f in findings if f.level == "WARN"] + + for f in errors + warns: + print(f"{f.level}: {f.path}: {f.message}") + + if errors: + return 1 + if warns and args.strict: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))