feat(homeassistant): waiting updates, backup age, and updates installed after a confirmed backup (#6355)
A new plugin reads Home Assistant with a read-only token every 5 minutes and shows which Core, OS and add-on updates are waiting and how old the last backup is. Install takes a backup through a local-only webhook, confirms Home Assistant's last-backup time moved, installs, and follows the update to its end, through Home Assistant's and Steward's own restarts. One runs at a time. Each ends as one timeline entry. Steward holds nothing that can act on the house: the webhook's automation can only take a backup or install an allowed update (System 231's ruling). A refused token stops polling, as Home Assistant bans an address after a few refusals. Milestone 576 steps 1 to 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
{# plugins/homeassistant/templates/homeassistant/index.html — whether Home
|
||||
Assistant answers, which updates are waiting with Install beside each, how its
|
||||
backups stand, and the updates Steward has run. Refreshes itself while one is
|
||||
under way. #}
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import page_header, status_badge, stat_tile, empty_state %}
|
||||
{% block title %}Home Assistant — Steward{% endblock %}
|
||||
{% block content %}
|
||||
{% set admin = session.user_role == 'admin' %}
|
||||
{% call page_header("Home Assistant", connection.url if connection else none) %}
|
||||
{% if admin and connection %}<a class="btn btn-sm btn-ghost" href="/plugins/homeassistant/connection">Connection</a>{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% if not connection %}
|
||||
<div class="card">{{ empty_state("Not connected. Connect a Home Assistant instance to see its waiting updates and backups, and to install updates after a confirmed backup.",
|
||||
"Connect" if admin else none, "/plugins/homeassistant/connection") }}</div>
|
||||
{% else %}
|
||||
{% if error %}<p class="field-error" role="alert">{{ error }}</p>{% endif %}
|
||||
<div id="ha-body"{% if under_way %} hx-get="/plugins/homeassistant/" hx-select="#ha-body" hx-trigger="every 15s" hx-swap="outerHTML"{% endif %}>
|
||||
<p class="page-verdict">
|
||||
{% if under_way %}{{ status_badge('pending', 'Updating') }}
|
||||
{{ under_way.title }}: {{ "taking a backup first" if under_way.state == 'backing_up' else "installing " ~ under_way.to_version }}. Home Assistant may stop answering for a while.
|
||||
{% elif reach == 'up' %}{{ status_badge('up', 'Answering') }}
|
||||
{% if installable %}{{ installable|length }} update{{ "s" if installable|length != 1 }} waiting.{% else %}Everything Steward can update is up to date.{% endif %}
|
||||
Read {{ connection.last_polled_at|ago }}.
|
||||
{% elif reach == 'down' %}{{ status_badge('down', 'Not answering') }} <span class="tone-crit">{{ says }}</span>
|
||||
{% if connection.last_ok_at %}Last answered {{ connection.last_ok_at|ago }}; what's below is from then.{% endif %}
|
||||
{% if admin and connection.refused %}<form method="post" action="/plugins/homeassistant/connection/retry" style="display:inline;">{{ csrf_field() }}<button class="btn btn-sm btn-ghost" type="submit">Try again</button></form>{% endif %}
|
||||
{% else %}{{ status_badge('pending', 'Not read yet') }} {{ says }}{% endif %}
|
||||
</p>
|
||||
|
||||
{% if connection.last_ok_at and connection.automation != 'on' %}
|
||||
<p class="tone-warn">{% if connection.automation %}The Steward maintenance automation is {{ connection.automation }} in Home Assistant{% else %}The Steward maintenance automation isn't in Home Assistant{% endif %}, so updates can't be installed from here.
|
||||
{% if admin %}<a href="/plugins/homeassistant/connection">See what to add.</a>{% endif %}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="stat-strip">
|
||||
{{ stat_tile("Updates waiting", installable|length if connection.last_ok_at else "—") }}
|
||||
{{ stat_tile("Last good backup", (backup_at|ago) if backup_at else "—", state="degraded" if backup_tone == 'warn' else none) }}
|
||||
{{ stat_tile("Next backup", (backup_next|ago) if backup_next else "—") }}
|
||||
</div>
|
||||
<p class="{{ 'tone-warn' if backup_tone == 'warn' else 'field-hint' }}">{{ backup_says }}</p>
|
||||
|
||||
<section class="card" aria-labelledby="ha-updates">
|
||||
<h2 id="ha-updates" class="section-title">Updates</h2>
|
||||
{% if installable %}
|
||||
<table class="table">
|
||||
<thead><tr><th scope="col">What</th><th scope="col">Now</th><th scope="col">New</th>{% if admin %}<th scope="col"></th>{% endif %}</tr></thead>
|
||||
<tbody>
|
||||
{% for u in installable %}
|
||||
<tr>
|
||||
<td>{{ u.title }}<div class="cell-sub">{{ u.kind_word }}{% if u.auto_update %}, updates itself{% endif %}</div></td>
|
||||
<td class="mono">{{ u.installed or "—" }}</td>
|
||||
<td class="mono">{% if u.release_url %}<a href="{{ u.release_url }}" rel="noopener noreferrer" target="_blank">{{ u.latest or "—" }}</a>{% else %}{{ u.latest or "—" }}{% endif %}</td>
|
||||
{% if admin %}<td style="text-align:right;">
|
||||
{% if connection.automation == 'on' and not under_way and not connection.refused and not u.in_progress %}
|
||||
<form method="post" action="/plugins/homeassistant/install"
|
||||
data-confirm="Install {{ u.title }} {{ u.latest }}? Steward takes a backup first and won't install without it. {{ 'Home Assistant restarts.' if u.kind == 'core' else ('The machine reboots.' if u.kind == 'os' else 'The add-on restarts.') }}">{{ csrf_field() }}
|
||||
<input type="hidden" name="entity_id" value="{{ u.entity_id }}">
|
||||
<button class="btn btn-sm" type="submit">Install</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="field-hint" style="margin:0;">Nothing waiting.</p>
|
||||
{% endif %}
|
||||
{% if current %}<p class="field-hint">Up to date: {% for u in current %}{{ u.title }} {{ u.installed }}{{ ", " if not loop.last }}{% endfor %}.</p>{% endif %}
|
||||
{% if others %}<p class="field-hint" style="margin-bottom:0;">Waiting, for Home Assistant to install: {% for u in others %}{{ u.title }}{{ ", " if not loop.last }}{% endfor %}. Steward installs Core, OS and add-on updates only.</p>{% endif %}
|
||||
</section>
|
||||
|
||||
{% if runs %}
|
||||
<section class="card" aria-labelledby="ha-runs">
|
||||
<h2 id="ha-runs" class="section-title">Updates run from Steward</h2>
|
||||
<table class="table">
|
||||
<thead><tr><th scope="col">What</th><th scope="col">Versions</th><th scope="col">Outcome</th><th scope="col">Started</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in runs %}
|
||||
<tr>
|
||||
<td>{{ r.title }}<div class="cell-sub">by {{ r.started_by or "Steward" }}</div></td>
|
||||
<td class="mono">{{ r.from_version or "—" }} → {{ r.to_version or "—" }}</td>
|
||||
<td>{% if r.state == 'done' %}{{ status_badge('up', 'Done') }}
|
||||
{% elif r.state == 'failed' %}{{ status_badge('down', 'Failed') }}<div class="cell-sub">{{ r.message }}</div>
|
||||
{% else %}{{ status_badge('pending', 'Taking a backup' if r.state == 'backing_up' else 'Installing') }}{% endif %}</td>
|
||||
<td>{{ r.started_at|ago }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user