diff --git a/config/dashboards/infrastructure/partials/joanna_sections.yaml b/config/dashboards/infrastructure/partials/joanna_sections.yaml index f40f28a1..0f85cfb2 100644 --- a/config/dashboards/infrastructure/partials/joanna_sections.yaml +++ b/config/dashboards/infrastructure/partials/joanna_sections.yaml @@ -34,6 +34,16 @@ entity: sensor.joanna_ha_errors_24h name: HA Errors 24H icon: mdi:alert-circle-outline + - type: custom:button-card + template: bearstone_infra_kpi + entity: sensor.codex_reset_credits_available + name: Resets + icon: mdi:backup-restore + - type: custom:button-card + template: bearstone_infra_kpi + entity: sensor.codex_reset_credits_expiring_soon + name: Reset Warnings + icon: mdi:alert-clock - type: custom:button-card template: bearstone_infra_kpi entity: sensor.joanna_all_dispatches_24h @@ -174,6 +184,114 @@ return `${enabled ? 'enabled' : 'disabled'} | ${age} min ago`; ]]] +- type: grid + column_span: 4 + columns: 1 + square: false + 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: Codex Reset Credits + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.codex_reset_credit_status + name: Collector Status + icon: mdi:clipboard-pulse-outline + state_display: > + [[[ + const usage = states['sensor.bearclaw_status_telemetry']?.attributes?.codexUsage || {}; + const status = usage.status || 'unknown'; + const ok = usage.ok === true; + const last = usage.lastCheckAt ? String(usage.lastCheckAt).replace('T', ' ').slice(0, 16) : 'not checked'; + return `${ok ? 'ok' : status} | ${last}`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.codex_reset_credit_next_expiration + name: Next Expiration + icon: mdi:calendar-clock + state_display: > + [[[ + const usage = states['sensor.bearclaw_status_telemetry']?.attributes?.codexUsage || {}; + const days = usage.minDaysRemaining; + const expRaw = usage.soonestExpiresAt || ''; + const exp = expRaw ? String(expRaw).replace('T', ' ').slice(0, 16) : 'n/a'; + const dayText = Number.isFinite(Number(days)) ? `${Number(days).toFixed(1)}d` : 'n/a'; + return `${dayText} | ${exp}`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: binary_sensor.codex_reset_credit_expiring_soon + name: Repair Window + icon: mdi:alert-clock + state_display: > + [[[ + const usage = states['sensor.bearclaw_status_telemetry']?.attributes?.codexUsage || {}; + const warn = usage.warnDays ?? 7; + const count = usage.warningCount ?? 0; + return `${count} within ${warn}d`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.bearclaw_status_telemetry + name: Reset #1 + icon: mdi:backup-restore + state_display: > + [[[ + const credits = states['sensor.bearclaw_status_telemetry']?.attributes?.codexUsage?.credits || []; + const credit = credits[0]; + if (!credit) return 'No reset'; + const days = Number.isFinite(Number(credit.days_remaining)) ? `${Number(credit.days_remaining).toFixed(1)}d` : 'n/a'; + const exp = credit.expires_at ? String(credit.expires_at).replace('T', ' ').slice(0, 16) : 'n/a'; + return `${credit.status || 'unknown'} | ${days} | ${exp}`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.bearclaw_status_telemetry + name: Reset #2 + icon: mdi:backup-restore + state_display: > + [[[ + const credits = states['sensor.bearclaw_status_telemetry']?.attributes?.codexUsage?.credits || []; + const credit = credits[1]; + if (!credit) return 'No reset'; + const days = Number.isFinite(Number(credit.days_remaining)) ? `${Number(credit.days_remaining).toFixed(1)}d` : 'n/a'; + const exp = credit.expires_at ? String(credit.expires_at).replace('T', ' ').slice(0, 16) : 'n/a'; + return `${credit.status || 'unknown'} | ${days} | ${exp}`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.bearclaw_status_telemetry + name: Reset #3 + icon: mdi:backup-restore + state_display: > + [[[ + const credits = states['sensor.bearclaw_status_telemetry']?.attributes?.codexUsage?.credits || []; + const credit = credits[2]; + if (!credit) return 'No reset'; + const days = Number.isFinite(Number(credit.days_remaining)) ? `${Number(credit.days_remaining).toFixed(1)}d` : 'n/a'; + const exp = credit.expires_at ? String(credit.expires_at).replace('T', ' ').slice(0, 16) : 'n/a'; + return `${credit.status || 'unknown'} | ${days} | ${exp}`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.bearclaw_status_telemetry + name: Reset #4 + icon: mdi:backup-restore + state_display: > + [[[ + const credits = states['sensor.bearclaw_status_telemetry']?.attributes?.codexUsage?.credits || []; + const credit = credits[3]; + if (!credit) return 'No reset'; + const days = Number.isFinite(Number(credit.days_remaining)) ? `${Number(credit.days_remaining).toFixed(1)}d` : 'n/a'; + const exp = credit.expires_at ? String(credit.expires_at).replace('T', ' ').slice(0, 16) : 'n/a'; + return `${credit.status || 'unknown'} | ${days} | ${exp}`; + ]]] + - type: grid column_span: 4 columns: 1 @@ -204,5 +322,7 @@ hours_to_show: 72 entities: - automation.bearclaw_reply_webhook + - automation.codex_reset_credit_expiry_repair - automation.tugtainer_dispatch_joanna_for_available_updates + - binary_sensor.codex_reset_credit_expiring_soon - script.joanna_dispatch diff --git a/config/dashboards/infrastructure/views/06_joanna.yaml b/config/dashboards/infrastructure/views/06_joanna.yaml index dbfabf37..d630f8fb 100644 --- a/config/dashboards/infrastructure/views/06_joanna.yaml +++ b/config/dashboards/infrastructure/views/06_joanna.yaml @@ -7,6 +7,7 @@ # Dispatch and activity telemetry for BearClaw/Joanna. # ------------------------------------------------------------------- # Notes: Uses telemetry from /api/bearclaw/status ingested by bearclaw.yaml. +# Notes: Shows Codex reset-credit expiry data from the codexUsage status field. ###################################################################### type: sections diff --git a/config/packages/README.md b/config/packages/README.md index ea0d955a..277c3b69 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -50,7 +50,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [mariadb_monitoring.yaml](mariadb_monitoring.yaml) | MariaDB health sensors and Lovelace dashboard snippet for recorder stats. | `sensor.mariadb_status`, `sensor.database_size` | | [llmvision.yaml](llmvision.yaml) | Vision-backed garage-can and front-door package checks with rate-limited, downscaled OpenAI calls for package detection; see the [package reminder video](https://youtu.be/nAhCezFetvI). | `input_button.llmvision_*`, `binary_sensor.front_door_packages_present`, `llmvision.stream_analyzer` | | [docker_infrastructure.yaml](docker_infrastructure.yaml) | Docker host patching telemetry, container/stack Repairs automation, retired Portainer repair cleanup, 20-minute Joanna escalation for persistent container outages using stable configured monitor membership, and weekly scheduled prune actions across docker_10/14/17/69; the dedicated codex_appliance VM is monitored through BearClaw status telemetry. | `sensor.docker_*_apt_status`, `binary_sensor.*_stack_status`, `sensor.docker_stacks_down_count`, `repairs.create`, `repairs.remove`, `script.joanna_dispatch` | -| [proxmox.yaml](proxmox.yaml) | Proxmox runtime and disk pressure monitoring with Repairs + Joanna dispatch for sustained node degradations, plus nightly Frigate reboot. | `binary_sensor.proxmox*_runtime_healthy`, `sensor.proxmox*_disk_used_percentage`, `repairs.create`, `script.joanna_dispatch`, `button.qemu_docker2_101_reboot` | +| [proxmox.yaml](proxmox.yaml) | Proxmox update detection with HA notifications, Repairs + Joanna upgrade orchestration, kernel-refresh handoff hints, runtime and disk pressure monitoring, plus nightly Frigate reboot. | `binary_sensor.node_proxmox*_updates_packages`, `sensor.node_proxmox*_total_updates`, `persistent_notification.create`, `script.joanna_dispatch`, `binary_sensor.proxmox*_runtime_healthy`, `sensor.proxmox*_disk_used_percentage`, `button.qemu_docker2_101_reboot` | | [synology_dsm.yaml](synology_dsm.yaml) | Synology DSM integration health normalization for Carlo-NAS01 and Carlo-NVR, with outage-aware Joanna-first handling for lone post-outage volume warnings and Repairs escalation for persistent or non-outage problems. | `binary_sensor.carlo_*_synology_problem`, `sensor.carlo_*_synology_problem_summary`, `binary_sensor.powerwall_grid_status`, `repairs.create`, `script.joanna_dispatch` | | [infrastructure.yaml](infrastructure.yaml) | Normalized WAN/DNS/backup/domain/cert health, Nebula Sync and promoted IoT primary/backup Pi-hole consistency monitoring with Joanna dispatch, Glances-backed Docker host disk pressure with Joanna-only warning cleanup and critical Repairs, and website uptime/latency SLO signals for Infrastructure dashboards, plus nightly backup verification and monthly Joanna HA log hygiene review with GitHub issue follow-up. | `sensor.infra_nebula_sync_dns_consistency`, `sensor.infra_pihole_iot_dns_consistency`, `binary_sensor.infra_nebula_sync_degraded`, `binary_sensor.infra_pihole_iot_dns_degraded`, `sensor.docker_*_disk_used_percentage`, `automation.infra_nebula_sync_health_dispatch`, `automation.infra_pihole_iot_dns_drift_dispatch`, `automation.docker_host_disk_pressure_monitor`, `binary_sensor.infra_website_uptime_slo_breach`, `binary_sensor.infra_website_latency_degraded`, `automation.infra_backup_nightly_verification`, `script.joanna_dispatch` | | [onenote_indexer.yaml](onenote_indexer.yaml) | Dedicated-appliance OneNote indexer health/status monitoring for Joanna, explicit index-health confirmation, failure-repair automation, and a daily duplicate-delete maintenance request. | `sensor.onenote_indexer_last_job_status`, `binary_sensor.onenote_indexer_last_job_successful`, `binary_sensor.onenote_indexer_index_healthy` | @@ -59,7 +59,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [processmonitor.yaml](processmonitor.yaml) | Root filesystem disk-pressure monitoring with immediate digest/logbook notes at 80%, Joanna review after 10 minutes above 80%, and delayed phone alerts only if the issue stays unresolved after dispatch. | `sensor.disk_use_percent`, `repairs.create`, `script.joanna_dispatch`, `tts.clear_cache` | | [tugtainer_updates.yaml](tugtainer_updates.yaml) | Tugtainer container update notifications via webhook + persistent alerts, plus event-based Joanna dispatch when reports include `### Available:` (24h cooldown via `mode: single` + delay, no new helpers). | `persistent_notification.create`, `event: tugtainer_available_detected`, `script.joanna_dispatch`, `input_datetime.tugtainer_last_update` | | [printer.yaml](printer.yaml) | Epson ink watchdog with one-day and one-week mobile snooze actions for low-ink reminders. | `input_datetime.printer_ink_snooze_until`, `sensor.epson_*`, mobile app action events | -| [bearclaw.yaml](bearclaw.yaml) | Joanna/BearClaw bridge automations that forward Telegram commands to codex_appliance, include LLM-first routing context for freeform text, relay replies back, ingest `/api/bearclaw/status` telemetry, and expose dispatch plus QMD/memory-index sensors for Infrastructure dashboards. | `rest_command.bearclaw_*`, `sensor.bearclaw_status_telemetry`, `sensor.joanna_*`, `binary_sensor.joanna_*`, `automation.bearclaw_*`, `script.send_to_logbook` | +| [bearclaw.yaml](bearclaw.yaml) | Joanna/BearClaw bridge automations that forward Telegram commands to codex_appliance, include LLM-first routing context for freeform text, relay replies back, ingest `/api/bearclaw/status` telemetry, and expose dispatch, QMD/memory-index, plus Codex reset-credit expiry sensors and Repairs for Infrastructure dashboards. | `rest_command.bearclaw_*`, `sensor.bearclaw_status_telemetry`, `sensor.joanna_*`, `sensor.codex_reset_*`, `binary_sensor.joanna_*`, `binary_sensor.codex_reset_credit_expiring_soon`, `automation.bearclaw_*`, `automation.codex_reset_credit_expiry_repair`, `repairs.create`, `repairs.remove`, `script.send_to_logbook` | | [telegram_bot.yaml](telegram_bot.yaml) | Legacy Telegram transport marker for BearClaw; the shared `joanna_send_telegram` helper now forwards through the codex_appliance direct Telegram API. | `rest_command.bearclaw_telegram_send`, `script.joanna_send_telegram` | | [phynplus.yaml](phynplus.yaml) | Phyn shutoff automations with leak-test guard, Activity Feed context, Repairs tracking, and critical push recovery when the valve closes; see the [leak detection automation video](https://youtu.be/xbhgWnomFYI). | `valve.phyn_shutoff_valve`, `binary_sensor.phyn_leak_test_running`, `script.phyn_send_actionable_leak_notification`, `repairs.create` | | [water_delivery.yaml](water_delivery.yaml) | ReadyRefresh delivery date helper with night-before + garage door Alexa reminders, plus helper-change audit logging and Telegram confirmations. | `input_datetime.water_delivery_date`, `script.send_to_logbook`, `script.joanna_send_telegram`, `notify.alexa_media_garage` | diff --git a/config/packages/bearclaw.yaml b/config/packages/bearclaw.yaml index 1a277780..67073783 100644 --- a/config/packages/bearclaw.yaml +++ b/config/packages/bearclaw.yaml @@ -6,12 +6,14 @@ # BearClaw Integration - Home Assistant dispatch + lifecycle callbacks # Home Assistant dispatches jobs to codex_appliance and receives structured lifecycle callbacks. # ------------------------------------------------------------------- +# Related Issue: 1797 # Notes: Telegram ingress now lives directly in the dedicated codex_appliance VM. # Notes: Most BearClaw decision logic runs in codex_appliance/server.js. # Notes: GitHub capture behavior (issue creation/labels/research flow) belongs in codex_appliance, not HA YAML. # Notes: Shared script helper `script.joanna_dispatch` lives in config/script/joanna_dispatch.yaml. # Notes: The callback webhook writes JOANNA activity entries to logbook for traceability and optional HA alerts. # Notes: Status telemetry polling expects !secret bearclaw_status_url (token header stays !secret bearclaw_token). +# Notes: Codex reset-credit expiry telemetry is sourced from the appliance `codexUsage` status field. # Notes: Nightly Duplicati verification calls a codex_appliance admin endpoint and returns structured health to HA via response_variable. # Notes: v2 intake is the primary HA contract; legacy command/ingest routes remain appliance-side shims. # Notes: Command payload supports async_only for automation-first queueing when immediate inline handling is not required. @@ -135,6 +137,7 @@ sensor: - platform - transports - followups + - codexUsage - qmdHealth - memoryIndex @@ -241,6 +244,70 @@ template: {% set elapsed_ms = active.get('elapsedMs', 0) | int(0) %} {{ (elapsed_ms / 60000) | round(1) }} + - name: Codex Reset Credits Available + unique_id: codex_reset_credits_available + icon: mdi:backup-restore + unit_of_measurement: resets + state_class: measurement + state: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('availableCount', 0) | int(0) }} + + - name: Codex Reset Credits Expiring Soon + unique_id: codex_reset_credits_expiring_soon + icon: mdi:alert-clock + unit_of_measurement: resets + state_class: measurement + state: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('warningCount', 0) | int(0) }} + + - name: Codex Reset Credit Min Days Remaining + unique_id: codex_reset_credit_min_days_remaining + icon: mdi:timer-sand + unit_of_measurement: d + state_class: measurement + availability: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('minDaysRemaining') not in [none, 'unknown', 'unavailable', ''] }} + state: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('minDaysRemaining', 0) | float(0) | round(1) }} + + - name: Codex Reset Credit Next Expiration + unique_id: codex_reset_credit_next_expiration + icon: mdi:calendar-clock + device_class: timestamp + availability: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('soonestExpiresAt') not in [none, 'unknown', 'unavailable', ''] }} + state: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('soonestExpiresAt') }} + + - name: Codex Reset Credit Status + unique_id: codex_reset_credit_status + icon: mdi:clipboard-pulse-outline + state: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('status', 'unknown') }} + attributes: + ok: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('ok', false) }} + last_check_at: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('lastCheckAt') }} + next_check_at: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('nextCheckAt') }} + last_error: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('lastError') }} + credits: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('credits', []) }} + - name: Joanna Minutes Since HA Dispatch unique_id: joanna_minutes_since_ha_dispatch icon: mdi:clock-fast @@ -320,7 +387,91 @@ template: state: >- {% set memory = state_attr('sensor.bearclaw_status_telemetry', 'memoryIndex') | default({}, true) %} {{ memory.get('stale', false) in [true, 'true', 'True', 'on', 'yes', 1, '1'] }} + + - name: Codex Reset Credit Expiring Soon + unique_id: codex_reset_credit_expiring_soon + icon: mdi:alert-clock + state: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('issueActive', false) in [true, 'true', 'True', 'on', 'yes', 1, '1'] }} + attributes: + warn_days: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('warnDays', 7) }} + warning_count: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('warningCount', 0) }} + soonest_expires_at: >- + {% set usage = state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) %} + {{ usage.get('soonestExpiresAt') }} automation: + - id: codex_reset_credit_expiry_repair + alias: Codex Reset Credit Expiry Repair + description: "Create/clear a Repair while available Codex reset credits are within the expiry warning window." + mode: single + trigger: + - platform: state + entity_id: binary_sensor.codex_reset_credit_expiring_soon + - platform: time_pattern + hours: "/6" + variables: + usage: "{{ state_attr('sensor.bearclaw_status_telemetry', 'codexUsage') | default({}, true) }}" + usage_ok: "{{ usage.get('ok', false) in [true, 'true', 'True', 'on', 'yes', 1, '1'] }}" + issue_active: "{{ is_state('binary_sensor.codex_reset_credit_expiring_soon', 'on') }}" + warn_days: "{{ usage.get('warnDays', 7) | int(7) }}" + warning_count: "{{ usage.get('warningCount', 0) | int(0) }}" + min_days: "{{ usage.get('minDaysRemaining') }}" + soonest_expires_at: "{{ usage.get('soonestExpiresAt') | default('unknown', true) }}" + last_check_at: "{{ usage.get('lastCheckAt') | default('unknown', true) }}" + expiring_summary: >- + {% set credits = usage.get('credits', []) %} + {% set ns = namespace(lines=[]) %} + {% for credit in credits %} + {% set status = credit.get('status', 'unknown') %} + {% set days = credit.get('days_remaining') %} + {% if status == 'available' and days is number and days <= warn_days %} + {% set ns.lines = ns.lines + [ + 'Reset #' ~ credit.get('index', loop.index) ~ ': ' ~ + (days | round(1)) ~ ' days remaining; expires ' ~ + credit.get('expires_at', 'unknown') + ] %} + {% endif %} + {% endfor %} + {{ ns.lines | join('\n') if ns.lines | count > 0 else 'No reset credits are inside the warning window.' }} + action: + - choose: + - conditions: "{{ issue_active }}" + sequence: + - service: repairs.create + data: + issue_id: codex_reset_credit_expiring_soon + title: "Codex reset credit expiring soon" + description: >- + {{ warning_count }} available Codex reset credit(s) expire within {{ warn_days }} days. + + Soonest expiration: {{ soonest_expires_at }} + Minimum days remaining: {{ min_days }} + Last successful/attempted check: {{ last_check_at }} + + {{ expiring_summary }} + severity: warning + persistent: true + - service: script.send_to_logbook + data: + topic: "CODEX" + message: >- + Codex reset credit warning active: {{ warning_count }} reset(s) within {{ warn_days }} days. + - conditions: "{{ usage_ok }}" + sequence: + - service: repairs.remove + continue_on_error: true + data: + issue_id: codex_reset_credit_expiring_soon + - service: script.send_to_logbook + data: + topic: "CODEX" + message: "Codex reset credit warning cleared after a successful reset snapshot." + - id: bearclaw_lifecycle_webhook alias: BearClaw Lifecycle Webhook description: Receives structured BearClaw lifecycle callbacks for HA-triggered work.