From a6105a34d49b2193c0634bc3a58ecd20b25f5003 Mon Sep 17 00:00:00 2001 From: Carlo Costanzo Date: Fri, 13 Feb 2026 19:05:32 -0500 Subject: [PATCH] Enhance Home Assistant dashboard configuration by adding new layout rules for sections, including mandatory full-width containers and improved alert row templates. Consolidate layout to eliminate dead space and ensure responsive design. Remove obsolete views for MariaDB and Activity Feed. Update README to reflect new infrastructure observability package. #1561 --- .../homeassistant-dashboard-designer/SKILL.md | 41 + .../references/dashboard_rules.md | 34 + .../partials/home_sections.yaml | 768 ++++++++++++++---- .../infra_top_chips_home_section.yaml | 35 + .../partials/pihole_sections.yaml | 28 + .../partials/proxmox_sections.yaml | 94 +-- .../partials/website_health_sections.yaml | 236 ++++++ .../templates/button_card_templates.yaml | 40 + .../infrastructure/views/04_pihole.yaml | 19 + .../{04_mariadb.yaml => 05_mariadb.yaml} | 0 .../views/07_website_health.yaml | 20 + ...tivity_feed.yaml => 08_activity_feed.yaml} | 0 config/packages/README.md | 1 + .../infrastructure_observability.yaml | 266 ++++++ 14 files changed, 1333 insertions(+), 249 deletions(-) create mode 100644 config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml create mode 100644 config/dashboards/infrastructure/partials/pihole_sections.yaml create mode 100644 config/dashboards/infrastructure/partials/website_health_sections.yaml create mode 100644 config/dashboards/infrastructure/views/04_pihole.yaml rename config/dashboards/infrastructure/views/{04_mariadb.yaml => 05_mariadb.yaml} (100%) create mode 100644 config/dashboards/infrastructure/views/07_website_health.yaml rename config/dashboards/infrastructure/views/{05_activity_feed.yaml => 08_activity_feed.yaml} (100%) create mode 100644 config/packages/infrastructure_observability.yaml diff --git a/codex_skills/homeassistant-dashboard-designer/SKILL.md b/codex_skills/homeassistant-dashboard-designer/SKILL.md index 9eb38e77..f6e7c7f6 100644 --- a/codex_skills/homeassistant-dashboard-designer/SKILL.md +++ b/codex_skills/homeassistant-dashboard-designer/SKILL.md @@ -78,6 +78,8 @@ Tier 2 (fallback, must justify in comments): - No freeform positioning - No layout logic embedded in `card-mod` - For dense tile lists in `type: sections` views, keep the outer panel full-width (`column_span: 4`) and make the inner tile grid responsive using `custom:layout-card` (`4` desktop / `2` mobile unless the user asks otherwise). +- In `type: sections` views, the first section immediately below top chips/badges must be a full-width container (`column_span: 4`) with a single wrapper card that also sets `grid_options.columns: full` to prevent skinny-column whitespace. +- Consolidate upward after removals/moves: do not leave dead space, empty placeholders, or intentionally sparse rows unless the user explicitly asks for whitespace. 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). @@ -138,14 +140,50 @@ Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, Max layout nesting depth: 2. No horizontal-stack inside grid cells. ``` +## Optional Stitch Handoff (Light Bridge) + +Use this only when the user explicitly asks for visual ideation, layout exploration, or variants. + +- Trigger Stitch path: + - User asks for redesign inspiration, variants, or multiple visual directions. +- Skip Stitch path: + - User asks for direct dashboard refactor/update and no ideation is needed. +- Preconditions: + - Stitch is used for hierarchy/spacing/grouping ideation only. + - Stitch output must never be copied as implementation code. + - Final implementation must still obey all allowed card/layout rules in this skill. + +Handoff artifact (summary only, no code export): + +```yaml +stitch_intent: + layout_pattern: "" + section_hierarchy: "
" + density_target: "" + cards_to_translate: + - "" +``` + +Translation requirement: +- Convert `stitch_intent` into approved Lovelace structures only (`grid`, `vertical-stack`, Tier 1 cards first). +- Re-map any unsupported Stitch concepts to compliant cards/containers and explain fallback choices inline when Tier 2 is required. + +Fallback behavior when Stitch is unavailable: +- Continue with manual ideation using this skill's design system and rules. +- State degraded mode clearly ("Stitch unavailable; proceeding with manual hierarchy/density planning"). +- Do not block dashboard work on Stitch availability. + ## Workflow (Do This In Order) 1. Read the target dashboard/view/partials/templates to understand existing patterns and avoid drift. 2. Determine intent from the user's request and existing dashboard context: `infra` (NOC), `home`, `energy`, or `environment`. Keep one intent per view. 3. Validate entities and services before editing: - Prefer the Home Assistant MCP for live entity/service validation (required when available). + - Record the MCP validation step in the work notes before writing YAML. - If MCP is not available, ask the user to confirm entity IDs and services (do not guess). 4. Draft layout with constraints: a top-level `grid` and optional `vertical-stack` groups. + - If using Stitch, first summarize `stitch_intent` and treat it as advisory input to this step. + - After removals, reflow cards/sections upward to collapse gaps and reduce empty rows. 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: @@ -155,6 +193,9 @@ Max layout nesting depth: 2. No horizontal-stack inside grid cells. - 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. +Example flow: +- User asks for redesign inspiration -> Stitch ideation with required constraints -> summarize `stitch_intent` -> translate to Lovelace-safe YAML -> run validation checks. + ## 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/references/dashboard_rules.md b/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md index 8883b657..d25cbd59 100644 --- a/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md +++ b/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md @@ -25,9 +25,12 @@ Tier 2 (fallback, only when Tier 1 cannot satisfy the requirement; justify inlin - No freeform positioning - No layout logic embedded in `card-mod` - For dense status/container lists in `type: sections` views, keep the panel full-width (`column_span: 4`) and use a responsive inner grid (`4` desktop / `2` mobile by default). +- In `type: sections` views, always place a full-width wrapper section directly below top chips/badges and set the wrapper card to `grid_options.columns: full`. +- Consolidate layout vertically after any move/remove; dead space is disallowed unless explicitly requested. 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. +- Prefer reducing section count and regrouping cards into earlier rows rather than leaving sparse trailing space. ## Template Architecture (Required) @@ -111,6 +114,37 @@ Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, Max layout nesting depth: 2. No horizontal-stack inside grid cells. ``` +## Optional Stitch Handoff (Light Bridge) + +Decision rule: +- Use Stitch only when the user requests visual ideation, variants, or exploration. +- For normal refactor/update requests, skip Stitch and implement directly with this skill's rules. + +Handoff artifact (advisory summary, not implementation code): + +```yaml +stitch_intent: + layout_pattern: "" + section_hierarchy: "
" + density_target: "" + cards_to_translate: + - "" +``` + +Required translation back to Lovelace: +- Treat `stitch_intent` as input to layout planning only. +- Implement using approved containers/cards and template architecture from this document. +- For any unsupported concept, re-map to compliant cards and justify Tier 2 fallback inline. + +Degraded mode: +- If Stitch or Stitch skills are unavailable, continue with manual hierarchy/spacing ideation. +- Do not block dashboard work on Stitch availability. + +Anti-drift checklist: +- No HTML/CSS export artifacts from Stitch output. +- No nesting-depth violations (max 2). +- No card-type drift outside approved Tier 1/Tier 2 lists. + ## Repo-Specific Dashboard YAML Rules (config/dashboards/**) If working in this repo's `config/dashboards/` tree: diff --git a/config/dashboards/infrastructure/partials/home_sections.yaml b/config/dashboards/infrastructure/partials/home_sections.yaml index 5d60959a..4034fe64 100644 --- a/config/dashboards/infrastructure/partials/home_sections.yaml +++ b/config/dashboards/infrastructure/partials/home_sections.yaml @@ -4,216 +4,626 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Infrastructure Partial - Home sections -# Rebuilt homepage layout (Stitch-inspired). +# Desktop-first infra overview: full-width hero, exceptions-first alerts. # ------------------------------------------------------------------- -# Notes: Standardized on `custom:button-card` + `custom:mini-graph-card` with `card_mod` polish. +# Notes: Default/light theme only; no dark-mode specific styling. +# Notes: Always keep a full-width container directly below chips. ###################################################################### -- !include /config/dashboards/infrastructure/partials/infra_top_chips_section.yaml +- !include /config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml +# ------------------------------------------------------------------- +# Home hero (mandatory full-width container under chips) +# ------------------------------------------------------------------- - type: grid column_span: 4 columns: 1 square: false cards: - - type: custom:button-card - template: bearstone_infra_kpi - entity: sensor.total_wifi_clients - name: Wi-Fi Clients - icon: mdi:wifi - -- type: grid - column_span: 3 - columns: 3 - square: false - cards: - - type: custom:vertical-stack-in-card - card_mod: - style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + - type: custom:layout-card + grid_options: + columns: full + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) cards: - - type: custom:button-card - template: bearstone_infra_panel_header - name: Proxmox01 - - type: custom:mini-graph-card - name: CPU / Memory - icon: mdi:server - hours_to_show: 24 - points_per_hour: 2 - line_width: 2 - animate: true - show: - fill: true - legend: false - icon: true - name: true - state: true - entities: - - entity: sensor.node_proxmox1_cpu_used - name: CPU - - entity: sensor.node_proxmox1_memory_used_percentage - name: Memory - - type: grid - columns: 3 - square: false + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml cards: - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker69_169_status - name: Docker69 - icon: mdi:docker - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker69_169_last_boot']?.state ?? 'unknown'); ]]] + template: bearstone_infra_panel_header + name: Alerts + + - type: custom:button-card + template: bearstone_infra_list_row + name: Network + icon: mdi:access-point-network + show_state: false + tap_action: + action: none + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.unifi_ap_office_clients + name: Office AP has 0 clients + icon: mdi:wifi-alert variables: - button_entity: button.qemu_docker69_169_reboot - name: Docker69 - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_carlo_hass_105_status - name: HASS - icon: mdi:home-assistant - label: > - [[[ return "Last boot: " + (states['sensor.qemu_carlo_hass_105_last_boot']?.state ?? 'unknown'); ]]] + alert_kind: ap_zero + min_age_s: 300 + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.unifi_ap_study_clients + name: Study AP has 0 clients + icon: mdi:wifi-alert variables: - button_entity: button.qemu_carlo_hass_105_reboot - name: HASS - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_wireguard_104_status - name: WireGuard - icon: mdi:vpn - label: > - [[[ return "Last boot: " + (states['sensor.qemu_wireguard_104_last_boot']?.state ?? 'unknown'); ]]] + alert_kind: ap_zero + min_age_s: 300 + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.unifi_ap_garage_clients + name: Garage AP has 0 clients + icon: mdi:wifi-alert variables: - button_entity: button.qemu_wireguard_104_reboot - name: WireGuard + alert_kind: ap_zero + min_age_s: 300 - - type: custom:vertical-stack-in-card - card_mod: - style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml - cards: - - type: custom:button-card - template: bearstone_infra_panel_header - name: Proxmox02 - - type: custom:mini-graph-card - name: CPU / Memory - icon: mdi:server - hours_to_show: 24 - points_per_hour: 2 - line_width: 2 - animate: true - show: - fill: true - legend: false - icon: true - name: true - state: true - entities: - - entity: sensor.node_proxmox02_cpu_used - name: CPU - - entity: sensor.node_proxmox02_memory_used_percentage - name: Memory - - type: grid - columns: 1 - square: false - cards: - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker2_101_status - name: Frigate - icon: mdi:video - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker2_101_last_boot']?.state ?? 'unknown'); ]]] + template: bearstone_infra_alert_row + entity: sensor.speedtest_download + name: Internet speed is slow + icon: mdi:speedometer-slow variables: - button_entity: button.qemu_docker2_101_reboot - name: Frigate + alert_kind: speedtest_slow + threshold: 300 + state_display: > + [[[ + const dl = parseFloat(states['sensor.speedtest_download']?.state); + const ul = parseFloat(states['sensor.speedtest_upload']?.state); + const dlText = Number.isFinite(dl) ? (dl.toFixed(0) + ' Mbps') : 'n/a'; + const ulText = Number.isFinite(ul) ? (ul.toFixed(0) + ' Mbps') : 'n/a'; + return `DL ${dlText} | UL ${ulText}`; + ]]] - - type: custom:vertical-stack-in-card - card_mod: - style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml - cards: - - type: custom:button-card - template: bearstone_infra_panel_header - name: Pi-hole - - type: custom:pi-hole - device_id: d69637da16f7d7f3626070582be59808 + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.speedtest_ping + name: WAN latency high + icon: mdi:wan + variables: + alert_kind: disk_high + threshold: 80 + state_display: > + [[[ return `${entity.state} ms`; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: switch.pi_hole + name: DNS Pi-hole disabled + icon: mdi:dns-outline + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/pihole + state_display: > + [[[ return `Switch ${entity.state}`; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.pihole_status + name: DNS Pi-hole service down + icon: mdi:dns + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/pihole + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.vcloudinfo_com + name: vcloudinfo.com is down + icon: mdi:web-cancel + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.www_kingcrafthomes_com + name: kingcrafthomes.com is down + icon: mdi:web-cancel + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + + - type: custom:button-card + template: bearstone_infra_list_row + name: Domain expiration critical + icon: mdi:domain-off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const raw = states[id]?.state; + const n = Number(raw); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'No data' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const n = Number(states[id]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 14) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Domain expiration warning + icon: mdi:domain + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const raw = states[id]?.state; + const n = Number(raw); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'No data' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const n = Number(states[id]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min >= 14 && min < 30) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Certificate expiration critical + icon: mdi:certificate-outline + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'Not available' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 14) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Certificate expiration warning + icon: mdi:certificate + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'Not available' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min >= 14 && min < 30) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: System + icon: mdi:alert-circle-outline + show_state: false + tap_action: + action: none + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.disk_use_percent + name: Home Assistant disk usage high + icon: mdi:harddisk + variables: + alert_kind: disk_high + threshold: 80 + state_display: > + [[[ return `${entity.state}% used`; ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Backup stale or failed + icon: mdi:backup-restore + state_display: > + [[[ + const s = states['sensor.dockerconfigs_backup_status']?.state ?? 'unknown'; + const e = states['sensor.dockerconfigs_backup_error_message']?.state ?? ''; + const d = states['sensor.dockerconfigs_backup_date']?.state; + let ageText = 'n/a'; + if (d && !['unknown','unavailable','none',''].includes(String(d).toLowerCase())) { + const dt = new Date(d); + if (!Number.isNaN(dt.getTime())) { + const h = (Date.now() - dt.getTime()) / 3600000; + ageText = `${h.toFixed(1)}h`; + } + } + return `Age ${ageText} | ${s}${e ? ' | error' : ''}`; + ]]] + styles: + card: + - display: > + [[[ + const status = String(states['sensor.dockerconfigs_backup_status']?.state ?? '').toLowerCase(); + const err = String(states['sensor.dockerconfigs_backup_error_message']?.state ?? '').toLowerCase(); + const d = states['sensor.dockerconfigs_backup_date']?.state; + let stale = false; + if (d && !['unknown','unavailable','none',''].includes(String(d).toLowerCase())) { + const dt = new Date(d); + if (!Number.isNaN(dt.getTime())) stale = ((Date.now() - dt.getTime()) / 3600000) > 24; + } + const failed = status.includes('fail') || status.includes('error') || err.length > 0; + return (stale || failed) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Services + icon: mdi:docker + show_state: false + tap_action: + action: none + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.docker_10_apt_reboot_required + name: docker_10 needs reboot + icon: mdi:restart-alert + variables: + alert_kind: binary_on + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/docker + state_display: > + [[[ return 'REBOOT REQUIRED'; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.docker_14_apt_reboot_required + name: docker_14 needs reboot + icon: mdi:restart-alert + variables: + alert_kind: binary_on + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/docker + state_display: > + [[[ return 'REBOOT REQUIRED'; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.docker_69_apt_reboot_required + name: docker_69 needs reboot + icon: mdi:restart-alert + variables: + alert_kind: binary_on + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/docker + state_display: > + [[[ return 'REBOOT REQUIRED'; ]]] + + - type: custom:auto-entities + show_empty: false + card: + type: custom:layout-card + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + card_param: cards + filter: + include: !include /config/dashboards/infrastructure/partials/docker_container_rows_include.yaml + exclude: + - state: 'on' + + - type: custom:vertical-stack-in-card grid_options: columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Datacenter + - type: custom:mini-graph-card + name: Garage Temp + icon: mdi:thermometer + hours_to_show: 96 + points_per_hour: 1 + line_width: 2 + smoothing: true + show: + legend: false + labels: false + name: true + icon: true + state: true + color_thresholds: + - value: 50 + color: '#f39c12' + - value: 120 + color: '#d35400' + - value: 145 + color: '#c0392b' + entities: + - entity: sensor.proxmox_garage_average_temperature + name: Garage + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + + - type: custom:layout-card + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_status + name: UPS Status + icon: mdi:battery-heart-variant + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_battery_charge + name: UPS Battery + icon: mdi:battery-70 + state_display: > + [[[ + const v = Number(entity?.state); + return Number.isFinite(v) ? `${v.toFixed(0)}%` : (entity?.state ?? 'unknown'); + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_battery_runtime + name: Runtime Remaining + icon: mdi:timer-outline + state_display: > + [[[ + const secs = parseInt(entity?.state, 10); + if (!Number.isFinite(secs)) return entity?.state ?? 'unknown'; + const hours = Math.floor(secs / 3600); + const mins = Math.floor((secs % 3600) / 60); + return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_status + name: On Battery + icon: mdi:battery-alert-variant-outline + state_display: > + [[[ + const s = String(entity?.state || '').toUpperCase(); + return s.includes('OB') ? 'Yes' : 'No'; + ]]] + +# ------------------------------------------------------------------- +# Overviews (desktop: 2 columns, mobile: stack) +# ------------------------------------------------------------------- +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:layout-card + grid_options: + columns: full + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - type: custom:vertical-stack-in-card + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Wi-Fi Overview + - type: custom:mini-graph-card + name: Clients + icon: mdi:wifi + hours_to_show: 24 + line_width: 2 + points_per_hour: 1 + smoothing: true + show: + graph: line + legend: false + labels: false + name: true + icon: true + state: true + entities: + - entity: sensor.total_wifi_clients + name: Total + show_state: true + show_graph: false + show_line: false + show_points: false + show_fill: false + show_legend: false + - entity: sensor.unifi_ap_office_clients + name: Office AP + show_state: true + - entity: sensor.unifi_ap_study_clients + name: Study AP + show_state: true + - entity: sensor.unifi_ap_garage_clients + name: Garage AP + show_state: true + + - type: custom:vertical-stack-in-card + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Internet Trend + - type: custom:mini-graph-card + name: Speedtest + icon: mdi:speedometer + hours_to_show: 24 + points_per_hour: 1 + line_width: 2 + smoothing: true + show: + fill: false + legend: false + labels: false + name: true + icon: true + state: true + entities: + - entity: sensor.speedtest_download + name: Download + - entity: sensor.speedtest_upload + name: Upload +# ------------------------------------------------------------------- +# Activity highlights (compact) +# ------------------------------------------------------------------- - type: grid column_span: 4 columns: 1 square: false cards: - type: custom:vertical-stack-in-card + grid_options: + columns: full card_mod: style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml cards: - type: custom:button-card template: bearstone_infra_panel_header - name: Wi-Fi Overview - - type: custom:mini-graph-card - name: Clients - icon: mdi:wifi + name: Activity Highlights + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/activity + - type: logbook + target: + entity_id: + - sensor.activity_feed hours_to_show: 24 - line_width: 2 - points_per_hour: 1 - smoothing: true - show: - graph: line - legend: false - labels: false - name: true - icon: true - state: true - entities: - - entity: sensor.total_wifi_clients - name: Total - show_state: true - show_graph: false - show_line: false - show_points: false - show_fill: false - show_legend: false - - entity: sensor.unifi_ap_office_clients - name: Office AP - show_state: true - - entity: sensor.unifi_ap_study_clients - name: Study AP - show_state: true - - entity: sensor.unifi_ap_garage_clients - name: Garage AP - show_state: true - - type: grid - columns: 3 - square: false - cards: - - type: custom:button-card - template: bearstone_infra_device_tile - name: Garage AP - icon: mdi:access-point - entity: sensor.unifi_ap_garage_clients - label: > - [[[ return "Uptime: " + (states['sensor.unifi_ap_garage_uptime']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.unifi_ap_garage_restart - name: Garage AP - - type: custom:button-card - template: bearstone_infra_device_tile - name: Office AP - icon: mdi:access-point - entity: sensor.unifi_ap_office_clients - label: > - [[[ return "Uptime: " + (states['sensor.unifi_ap_office_uptime']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.unifi_ap_office_restart - name: Office AP - - type: custom:button-card - template: bearstone_infra_device_tile - name: Study AP - icon: mdi:access-point - entity: sensor.unifi_ap_study_clients - label: > - [[[ return "Uptime: " + (states['sensor.unifi_ap_study_uptime']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.unifi_ap_study_restart - name: Study AP diff --git a/config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml b/config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml new file mode 100644 index 00000000..8631be6c --- /dev/null +++ b/config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml @@ -0,0 +1,35 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure Partial - Top status chips (Home) +# Home-only chips row (no Pi-hole control; DNS health is alert-only on Home). +# ------------------------------------------------------------------- +# Notes: Keep this lightweight; it should render fast on the Home view. +###################################################################### + +type: grid +column_span: 4 +columns: 3 +square: false +cards: +- type: custom:button-card + template: bearstone_infra_chip + entity: binary_sensor.node_proxmox1_updates_packages + name: Proxmox01 + icon: mdi:server + state_display: > + [[[ return entity.state === 'on' ? 'Updates pending' : 'Up to date'; ]]] +- type: custom:button-card + template: bearstone_infra_chip + entity: binary_sensor.node_proxmox02_updates_packages + name: Proxmox02 + icon: mdi:server + state_display: > + [[[ return entity.state === 'on' ? 'Updates pending' : 'Up to date'; ]]] +- type: custom:button-card + template: bearstone_infra_chip + entity: sensor.garage_ups_status + name: Garage UPS + icon: mdi:transmission-tower diff --git a/config/dashboards/infrastructure/partials/pihole_sections.yaml b/config/dashboards/infrastructure/partials/pihole_sections.yaml new file mode 100644 index 00000000..f3846f9f --- /dev/null +++ b/config/dashboards/infrastructure/partials/pihole_sections.yaml @@ -0,0 +1,28 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure Partial - Pi-hole sections +# Dedicated full-width Pi-hole panel. +# ------------------------------------------------------------------- +# Notes: Uses `custom:pi-hole` with existing device_id. +###################################################################### + +- !include /config/dashboards/infrastructure/partials/infra_top_chips_section.yaml + +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:pi-hole + device_id: d69637da16f7d7f3626070582be59808 + grid_options: + columns: full diff --git a/config/dashboards/infrastructure/partials/proxmox_sections.yaml b/config/dashboards/infrastructure/partials/proxmox_sections.yaml index 69799452..97c44ecf 100644 --- a/config/dashboards/infrastructure/partials/proxmox_sections.yaml +++ b/config/dashboards/infrastructure/partials/proxmox_sections.yaml @@ -39,7 +39,7 @@ - type: grid column_span: 4 - columns: 2 + columns: 3 square: false cards: - type: custom:vertical-stack-in-card @@ -93,40 +93,6 @@ name: WireGuard CPU - entity: sensor.qemu_wireguard_104_memory_used_percentage name: WireGuard MEM - - type: grid - columns: 3 - square: false - cards: - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker69_169_status - name: Docker69 - icon: mdi:docker - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker69_169_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_docker69_169_reboot - name: Docker69 - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_carlo_hass_105_status - name: HASS - icon: mdi:home-assistant - label: > - [[[ return "Last boot: " + (states['sensor.qemu_carlo_hass_105_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_carlo_hass_105_reboot - name: HASS - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_wireguard_104_status - name: WireGuard - icon: mdi:vpn - label: > - [[[ return "Last boot: " + (states['sensor.qemu_wireguard_104_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_wireguard_104_reboot - name: WireGuard - type: custom:vertical-stack-in-card card_mod: @@ -171,40 +137,28 @@ name: CPU - entity: sensor.qemu_docker2_101_memory_used_percentage name: MEM - - type: grid - columns: 3 - square: false - cards: - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker2_101_status - name: Frigate - icon: mdi:video - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker2_101_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_docker2_101_reboot - name: Frigate -- type: grid - column_span: 4 - columns: 1 - square: false - cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.proxmox_garage_average_temperature - name: Garage - - entity: sensor.pirateweather_temperature - name: Outside - name: Garage Temp - hours_to_show: 96 - color_thresholds: - - value: 50 - color: '#f39c12' - - value: 120 - color: '#d35400' - - value: 145 - color: '#c0392b' + - type: custom:vertical-stack-in-card card_mod: - style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Garage Temp + - type: custom:mini-graph-card + entities: + - entity: sensor.proxmox_garage_average_temperature + name: Garage + - entity: sensor.pirateweather_temperature + name: Outside + name: Garage Temp + hours_to_show: 96 + color_thresholds: + - value: 50 + color: '#f39c12' + - value: 120 + color: '#d35400' + - value: 145 + color: '#c0392b' + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml diff --git a/config/dashboards/infrastructure/partials/website_health_sections.yaml b/config/dashboards/infrastructure/partials/website_health_sections.yaml new file mode 100644 index 00000000..5d027871 --- /dev/null +++ b/config/dashboards/infrastructure/partials/website_health_sections.yaml @@ -0,0 +1,236 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure Partial - Website health sections +# Website uptime + domain expiry + certificate telemetry detail view. +# ------------------------------------------------------------------- +# Notes: Keep a full-width container directly below chips to avoid whitespace. +###################################################################### + +- !include /config/dashboards/infrastructure/partials/infra_top_chips_section.yaml + +# ------------------------------------------------------------------- +# Website hero (mandatory full-width container under chips) +# ------------------------------------------------------------------- +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:layout-card + grid_options: + columns: full + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Website Uptime + - type: custom:layout-card + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - type: custom:button-card + template: bearstone_infra_list_row_running + entity: binary_sensor.vcloudinfo_com + name: vcloudinfo.com + icon: mdi:web + state_display: > + [[[ return entity.state === 'on' ? 'UP' : (entity.state || 'unknown').toUpperCase(); ]]] + - type: custom:button-card + template: bearstone_infra_list_row_running + entity: binary_sensor.www_kingcrafthomes_com + name: kingcrafthomes.com + icon: mdi:web + state_display: > + [[[ return entity.state === 'on' ? 'UP' : (entity.state || 'unknown').toUpperCase(); ]]] + - type: custom:button-card + template: bearstone_infra_list_row_running + entity: binary_sensor.bear_stone + name: Bear Stone + icon: mdi:web-check + state_display: > + [[[ return entity.state === 'on' ? 'UP' : (entity.state || 'unknown').toUpperCase(); ]]] + + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Domain Expiry + - type: custom:layout-card + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.vcloudinfo_com_days_until_expiration + name: vcloudinfo.com + icon: mdi:calendar-clock + state_display: > + [[[ + const days = states['sensor.vcloudinfo_com_days_until_expiration']?.state ?? 'n/a'; + const expRaw = states['sensor.vcloudinfo_com_expires']?.state; + let exp = 'n/a'; + const s = String(expRaw ?? ''); + if (s && !['unknown','unavailable','none',''].includes(s.toLowerCase())) { + if (/^\d{4}-\d{2}-\d{2}/.test(s)) { + exp = s.slice(0, 10); + } else { + const dt = new Date(s); + if (!Number.isNaN(dt.getTime())) { + const y = dt.getFullYear(); + const m = String(dt.getMonth() + 1).padStart(2, '0'); + const d = String(dt.getDate()).padStart(2, '0'); + exp = `${y}-${m}-${d}`; + } + } + } + return `${days}d | ${exp}`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.kingcrafthomes_com_days_until_expiration + name: kingcrafthomes.com + icon: mdi:calendar-clock + state_display: > + [[[ + const days = states['sensor.kingcrafthomes_com_days_until_expiration']?.state ?? 'n/a'; + const expRaw = states['sensor.kingcrafthomes_com_expires']?.state; + let exp = 'n/a'; + const s = String(expRaw ?? ''); + if (s && !['unknown','unavailable','none',''].includes(s.toLowerCase())) { + if (/^\d{4}-\d{2}-\d{2}/.test(s)) { + exp = s.slice(0, 10); + } else { + const dt = new Date(s); + if (!Number.isNaN(dt.getTime())) { + const y = dt.getFullYear(); + const m = String(dt.getMonth() + 1).padStart(2, '0'); + const d = String(dt.getDate()).padStart(2, '0'); + exp = `${y}-${m}-${d}`; + } + } + } + return `${days}d | ${exp}`; + ]]] + +# ------------------------------------------------------------------- +# Certificate health +# ------------------------------------------------------------------- +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Certificate Health + - type: custom:button-card + template: bearstone_infra_list_row + name: Cert telemetry sensors + icon: mdi:certificate-outline + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + return keys.length === 0 ? 'Missing (domain expiry only)' : `${keys.length} sensor(s)`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + name: Minimum cert days remaining + icon: mdi:calendar-alert + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'Not available' : `${Math.round(min)} days`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + name: Cert warning (< 30d) + icon: mdi:alert-outline + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 30 && min >= 14) ? 'ALERT' : 'OK'; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + name: Cert critical (< 14d) + icon: mdi:alert-circle + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 14) ? 'ALERT' : 'OK'; + ]]] diff --git a/config/dashboards/infrastructure/templates/button_card_templates.yaml b/config/dashboards/infrastructure/templates/button_card_templates.yaml index e8e17ae2..63f52a86 100644 --- a/config/dashboards/infrastructure/templates/button_card_templates.yaml +++ b/config/dashboards/infrastructure/templates/button_card_templates.yaml @@ -381,6 +381,46 @@ bearstone_infra_list_row: - padding-left: 8px - white-space: nowrap +bearstone_infra_alert_row: + template: bearstone_infra_list_row + # Home view only. Uses JS visibility checks to keep Home "exceptions-only". + styles: + card: + - border-color: rgba(229,57,53,0.25) + - background: rgba(255,235,238,0.65) + - display: > + [[[ + const kind = (variables && variables.alert_kind) ? String(variables.alert_kind) : ''; + const minAge = (variables && variables.min_age_s !== undefined) ? Number(variables.min_age_s) : 0; + const thr = (variables && variables.threshold !== undefined) ? Number(variables.threshold) : null; + + const stateStr = String(entity && entity.state !== undefined ? entity.state : ''); + const lastChanged = entity && entity.last_changed ? new Date(entity.last_changed) : null; + // If we can't determine age, do not surface time-gated alerts. + const ageS = lastChanged ? ((Date.now() - lastChanged.getTime()) / 1000) : 0; + + const num = (entId) => { + const v = parseFloat(states[entId] ? states[entId].state : ''); + return Number.isFinite(v) ? v : null; + }; + + let show = false; + if (kind === 'binary_on') show = (stateStr === 'on'); + else if (kind === 'binary_off') show = (stateStr === 'off'); + else if (kind === 'ap_zero') show = (stateStr === '0' && ageS >= minAge); + else if (kind === 'disk_high') { + const v = parseFloat(stateStr); + show = Number.isFinite(v) && (thr !== null ? (v > thr) : (v > 80)); + } else if (kind === 'speedtest_slow') { + const dl = num('sensor.speedtest_download'); + const ul = num('sensor.speedtest_upload'); + const t = (thr !== null) ? thr : 300; + show = (dl !== null && dl < t) || (ul !== null && ul < t); + } + + return show ? 'block' : 'none'; + ]]] + bearstone_infra_list_row_running: template: bearstone_infra_list_row state: diff --git a/config/dashboards/infrastructure/views/04_pihole.yaml b/config/dashboards/infrastructure/views/04_pihole.yaml new file mode 100644 index 00000000..dfd4d917 --- /dev/null +++ b/config/dashboards/infrastructure/views/04_pihole.yaml @@ -0,0 +1,19 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure View - Pi-hole +# Dedicated Pi-hole view after Docker. +# ------------------------------------------------------------------- +# Notes: Full-width sections layout for Pi-hole telemetry/control card. +###################################################################### + +title: Pi-hole +path: pihole +type: sections +icon: mdi:pi-hole +theme: default +badges: [] +sections: !include /config/dashboards/infrastructure/partials/pihole_sections.yaml +max_columns: 4 diff --git a/config/dashboards/infrastructure/views/04_mariadb.yaml b/config/dashboards/infrastructure/views/05_mariadb.yaml similarity index 100% rename from config/dashboards/infrastructure/views/04_mariadb.yaml rename to config/dashboards/infrastructure/views/05_mariadb.yaml diff --git a/config/dashboards/infrastructure/views/07_website_health.yaml b/config/dashboards/infrastructure/views/07_website_health.yaml new file mode 100644 index 00000000..1f4a3739 --- /dev/null +++ b/config/dashboards/infrastructure/views/07_website_health.yaml @@ -0,0 +1,20 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure View - Website Health +# Website uptime + domain expiry + certificate telemetry details. +# ------------------------------------------------------------------- +# Notes: Home view surfaces only exceptions; this view carries detail. +###################################################################### + +title: Website Health +path: website-health +type: sections +icon: mdi:web-check +theme: default +badges: [] +sections: !include /config/dashboards/infrastructure/partials/website_health_sections.yaml +max_columns: 4 +cards: [] diff --git a/config/dashboards/infrastructure/views/05_activity_feed.yaml b/config/dashboards/infrastructure/views/08_activity_feed.yaml similarity index 100% rename from config/dashboards/infrastructure/views/05_activity_feed.yaml rename to config/dashboards/infrastructure/views/08_activity_feed.yaml diff --git a/config/packages/README.md b/config/packages/README.md index 3c081403..cd7c342f 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -46,6 +46,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [logbook_activity_feed.yaml](logbook_activity_feed.yaml) | Dummy `sensor.activity_feed` + helper to write clean Activity entries (Issue #1550). | `sensor.activity_feed`, `script.send_to_logbook` | | [mariadb_monitoring.yaml](mariadb_monitoring.yaml) | MariaDB health sensors and Lovelace dashboard snippet for recorder stats. | `sensor.mariadb_status`, `sensor.database_size` | | [docker_infrastructure.yaml](docker_infrastructure.yaml) | Docker host patching + staggered auto-reboot flow + container-down Repairs alerts. | `sensor.docker_*_apt_status`, `repairs.create`, `repairs.remove` | +| [infrastructure_observability.yaml](infrastructure_observability.yaml) | Normalized WAN/DNS/backup/domain/cert health sensors used by the Infrastructure Home + Website Health dashboards. | `binary_sensor.infra_*`, `sensor.infra_*`, `script.send_to_logbook` | | [mariadb.yaml](mariadb.yaml) | MariaDB recorder health and capacity SQL sensors. | `sensor.mariadb_status`, `sensor.database_size` | | [tugtainer_updates.yaml](tugtainer_updates.yaml) | Tugtainer container update notifications via webhook + persistent alerts. | `persistent_notification.create`, `input_datetime.tugtainer_last_update` | | [phynplus.yaml](phynplus.yaml) | Phyn shutoff automations with push + Activity feed + Repairs issues for leak events. | `valve.phyn_shutoff_valve`, `binary_sensor.phyn_leak_test_running`, `repairs.create` | diff --git a/config/packages/infrastructure_observability.yaml b/config/packages/infrastructure_observability.yaml new file mode 100644 index 00000000..66a782dc --- /dev/null +++ b/config/packages/infrastructure_observability.yaml @@ -0,0 +1,266 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure Observability - Normalized infra monitoring signals +# WAN/DNS/backup/website/domain/cert state normalized for dashboards. +# ------------------------------------------------------------------- +# Notes: Home dashboard consumes `infra_*` entities for exceptions-only alerts. +# Notes: Domain warning threshold is <30 days; critical threshold is <14 days. +###################################################################### + +command_line: + - sensor: + name: Infra WAN Packet Loss + unique_id: infra_wan_packet_loss + command: >- + ping -q -c 10 -W 1 1.1.1.1 2>/dev/null | + awk -F',' '/packet loss/ {gsub(/%| /, "", $3); print $3; found=1} + END {if (!found) print "unknown"}' + scan_interval: 300 + unit_of_measurement: "%" + + - sensor: + name: Infra WAN Latency Ms + unique_id: infra_wan_latency_ms + command: >- + ping -q -c 10 -W 1 1.1.1.1 2>/dev/null | + awk -F'/' '/^rtt|^round-trip/ {print $5; found=1} + END {if (!found) print "unknown"}' + scan_interval: 300 + unit_of_measurement: "ms" + + - sensor: + name: Infra External IP Fallback + unique_id: infra_external_ip_fallback + command: "curl -fsS https://api.ipify.org || echo unknown" + scan_interval: 900 + +template: + - sensor: + - name: "Infra External IP" + unique_id: infra_external_ip + state: >- + {% set primary = states('sensor.external_ip') | trim %} + {% set fallback = states('sensor.infra_external_ip_fallback') | trim %} + {% if primary not in ['unknown', 'unavailable', 'none', ''] %} + {{ primary }} + {% else %} + {{ fallback }} + {% endif %} + + - name: "Infra Backup Age Hours" + unique_id: infra_backup_age_hours + unit_of_measurement: "h" + state: >- + {% set stamp = states('sensor.dockerconfigs_backup_date') %} + {% set ts = as_datetime(stamp) %} + {% if ts is not none %} + {{ ((now() - ts).total_seconds() / 3600) | round(1) }} + {% else %} + unknown + {% endif %} + + - name: "Infra Domain Expiry Min Days" + unique_id: infra_domain_expiry_min_days + unit_of_measurement: "d" + state: >- + {% set ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.ipmer_com_days_until_expiration', + 'sensor.fordst_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ] %} + {% set ns = namespace(min=9999, any=false) %} + {% for id in ids %} + {% if expand(id) | count > 0 %} + {% set raw = states(id) %} + {% if raw not in ['unknown', 'unavailable', 'none', ''] %} + {% set ns.any = true %} + {% set val = raw | float(9999) %} + {% if val < ns.min %} + {% set ns.min = val %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% if ns.any %} + {{ ns.min | round(0) }} + {% else %} + unknown + {% endif %} + + - name: "Infra Cert Expiry Min Days" + unique_id: infra_cert_expiry_min_days + unit_of_measurement: "d" + state: >- + {% set ns = namespace(min=9999, any=false) %} + {% for item in states.sensor %} + {% if item.entity_id is search('(vcloudinfo|ipmer|fordst|kingcrafthomes).*(cert|ssl|tls)') %} + {% set raw = item.state %} + {% if raw not in ['unknown', 'unavailable', 'none', ''] %} + {% set value = raw | float(9999) %} + {% if value != 9999 %} + {% set ns.any = true %} + {% if value < ns.min %} + {% set ns.min = value %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% if ns.any %} + {{ ns.min | round(0) }} + {% else %} + unknown + {% endif %} + + - name: "Infra Cert Telemetry Count" + unique_id: infra_cert_telemetry_count + icon: mdi:counter + state: >- + {% set ns = namespace(count=0) %} + {% for item in states.sensor %} + {% if item.entity_id is search('(vcloudinfo|ipmer|fordst|kingcrafthomes).*(cert|ssl|tls)') %} + {% set ns.count = ns.count + 1 %} + {% endif %} + {% endfor %} + {{ ns.count }} + + - name: "Infra Website Down Count" + unique_id: infra_website_down_count + icon: mdi:counter + state: >- + {% set ids = [ + 'binary_sensor.vcloudinfo_com', + 'binary_sensor.ipmer_com', + 'binary_sensor.fordst_com', + 'binary_sensor.www_kingcrafthomes_com' + ] %} + {% set ns = namespace(count=0) %} + {% for id in ids %} + {% if expand(id) | count > 0 %} + {% set st = states(id) %} + {% if st in ['off', 'unknown', 'unavailable'] %} + {% set ns.count = ns.count + 1 %} + {% endif %} + {% endif %} + {% endfor %} + {{ ns.count }} + + - binary_sensor: + - name: "Infra WAN Quality Degraded" + unique_id: infra_wan_quality_degraded + device_class: problem + state: >- + {% set loss_raw = states('sensor.infra_wan_packet_loss') %} + {% set lat_raw = states('sensor.infra_wan_latency_ms') %} + {% set invalid = loss_raw in ['unknown', 'unavailable', 'none', ''] or + lat_raw in ['unknown', 'unavailable', 'none', ''] %} + {% set loss = loss_raw | float(0) %} + {% set lat = lat_raw | float(0) %} + {{ invalid or loss > 5 or lat > 80 }} + + - name: "Infra Backup Stale Or Failed" + unique_id: infra_backup_stale_or_failed + device_class: problem + state: >- + {% set status = states('sensor.dockerconfigs_backup_status') | lower %} + {% set err = states('sensor.dockerconfigs_backup_error_message') | lower %} + {% set age = states('sensor.infra_backup_age_hours') | float(9999) %} + {% set failed = status in ['failed', 'failure', 'error', 'fatal'] or + 'fail' in status or + 'error' in status or + err not in ['unknown', 'unavailable', 'none', ''] %} + {{ failed or age > 24 }} + + - name: "Infra DNS Pihole Degraded" + unique_id: infra_dns_pihole_degraded + device_class: problem + state: >- + {% set switch_state = states('switch.pi_hole') %} + {% set service_state = states('binary_sensor.pihole_status') %} + {{ switch_state != 'on' or service_state in ['off', 'unavailable', 'unknown'] }} + + - name: "Infra UPS On Battery" + unique_id: infra_ups_on_battery + device_class: problem + state: >- + {% set status = states('sensor.garage_ups_status') | upper %} + {{ 'OB' in status }} + + - name: "Infra Website Degraded" + unique_id: infra_website_degraded + device_class: problem + state: >- + {{ states('sensor.infra_website_down_count') | int(0) > 0 }} + + - name: "Infra Domain Expiry Critical" + unique_id: infra_domain_expiry_critical + device_class: problem + state: >- + {% set d = states('sensor.infra_domain_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {{ d | float(9999) < 14 }} + {% endif %} + + - name: "Infra Domain Expiry Warning" + unique_id: infra_domain_expiry_warning + device_class: problem + state: >- + {% set d = states('sensor.infra_domain_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {% set days = d | float(9999) %} + {{ days < 30 and days >= 14 }} + {% endif %} + + - name: "Infra Cert Expiry Critical" + unique_id: infra_cert_expiry_critical + device_class: problem + state: >- + {% set d = states('sensor.infra_cert_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {{ d | float(9999) < 14 }} + {% endif %} + + - name: "Infra Cert Expiry Warning" + unique_id: infra_cert_expiry_warning + device_class: problem + state: >- + {% set d = states('sensor.infra_cert_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {% set days = d | float(9999) %} + {{ days < 30 and days >= 14 }} + {% endif %} + +automation: + - alias: "Infrastructure - External IP Change Logbook" + id: infra_external_ip_change_logbook + description: "Log external IP changes into the Activity feed." + mode: queued + trigger: + - platform: state + entity_id: sensor.infra_external_ip + condition: + - condition: template + value_template: "{{ trigger.from_state is not none }}" + - condition: template + value_template: >- + {{ trigger.from_state.state not in ['unknown', 'unavailable', 'none', ''] and + trigger.to_state.state not in ['unknown', 'unavailable', 'none', ''] and + trigger.from_state.state != trigger.to_state.state }} + action: + - service: script.send_to_logbook + data: + topic: "NETWORK" + message: >- + External IP changed from {{ trigger.from_state.state }} to {{ trigger.to_state.state }}.