Files
ros_control/app/ui/templates/dashboard.html
T
ayurishchevandClaude Opus 5.5 5f7bd6588a Состояния «Upgrade FW» и осознанный откат прошивки RouterBOARD
Пункт 18 ревью 2026-09-28 21:32 (docs/changes/027): после отката ROS
плата остаётся на более новой прошивке, а приложение сравнивало версии
на неравенство и показывало откат как обновление.
- devices.fw_state: unknown/update/downgrade/current; has_fw_update и
  фильтры не считают откат обновлением; метка «в ROS: X».
- upgrade_firmware не понижает прошивку; общая запись — _flash_firmware.
- Задача fw_downgrade: проверка → обязательный бэкап → запись →
  перезагрузка; API POST /devices/{id}/firmware/downgrade и
  /batch/fw_downgrade (target_version обязателен).
- UI: «Откатить прошивку…» (строка и группа), общее окно отката с kind.

Тесты: 39 из 39. Стенд: временное недоступное устройство — 422/202,
задачи failed на проверке, бэкапов нет. Реальная запись прошивки при
откате и ручная проверка UI не выполнялись.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:04:51 +03:00

88 lines
6.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% import "_ui.html" as ui %}
{% block content %}
<div class="page-head">
<div>
<h1>Устройства</h1>
<div class="sub" id="dev-sub">{% include "_dev_sub.html" %}</div>
</div>
<div class="actions">
<button type="button" class="btn secondary" hx-post="/ui/refresh" hx-include="#dev-filters" hx-target="#devices" hx-indicator="#busy">{{ ui.icon("refresh") }}Обновить статус</button>
<button type="button" class="btn primary" hx-get="/ui/dialog/device{{ '?group=' ~ flt.group if flt.group.startswith('grp_') }}" hx-target="#modal-body">{{ ui.icon("plus") }}Добавить устройство</button>
</div>
</div>
<nav class="tabs" aria-label="Группы устройств">
{% for t in tabs %}<a class="tab {{ 'on' if t.active }}" href="{{ t.href }}">{{ t.label }} <span class="count">{{ t.count }}</span></a>{% endfor %}
<span class="grow"></span>
<a class="tab-link" href="/groups">Управление группами {{ ui.icon("arrow", 14) }}</a>
</nav>
<form id="move-form" method="post" action="/ui/move" hidden><input type="hidden" name="next" value="{{ page_url }}"></form>
<div class="card" id="dev-card">
<div class="tools" id="tools-filters">
<form id="dev-filters" hx-get="/ui/devices" hx-target="#devices" hx-trigger="input delay:300ms, change" onsubmit="return false">
<input type="hidden" name="f_group" value="{{ flt.group }}">
<label class="search">{{ ui.icon("search") }}<input type="search" name="f_q" value="{{ flt.q }}" placeholder="Поиск: имя, адрес, модель" aria-label="Поиск: имя, адрес, модель"></label>
<select class="field {{ 'on' if flt.status }}" name="f_status" aria-label="Статус" style="width:150px">
<option value="">Статус: любой</option>
<option value="online" {{ 'selected' if flt.status == 'online' }}>Статус: online</option>
<option value="offline" {{ 'selected' if flt.status == 'offline' }}>Статус: offline</option>
</select>
<select class="field {{ 'on' if flt.updates }}" name="f_updates" aria-label="Обновления" style="width:190px">
<option value="">Обновления: любые</option>
<option value="any" {{ 'selected' if flt.updates == 'any' }}>Есть обновление</option>
<option value="ros" {{ 'selected' if flt.updates == 'ros' }}>Есть обновление ROS</option>
<option value="fw" {{ 'selected' if flt.updates == 'fw' }}>Есть обновление FW</option>
<option value="none" {{ 'selected' if flt.updates == 'none' }}>Всё актуально</option>
</select>
<select class="field {{ 'on' if flt.channel }}" name="f_channel" aria-label="Канал" style="width:150px">
<option value="">Канал: любой</option>
{% for c in channels %}<option value="{{ c }}" {{ 'selected' if flt.channel == c }}>Канал: {{ c }}</option>{% endfor %}
</select>
<span class="grow"></span>
<a id="filters-reset" class="btn ghost" href="{{ '/?f_group=' ~ flt.group if flt.group else '/' }}" {{ 'hidden' if not flt_active }}>Сбросить ({{ flt_active }})</a>
<span id="busy" class="busy htmx-indicator">…</span>
</form>
</div>
<div class="tools bulk" id="tools-bulk" hidden>
<span class="sel-count">Выбрано: <span id="sel-count">0</span></span>
<button type="button" class="btn secondary" hx-post="/ui/batch/backup" hx-include="[name=device_ids]:checked" hx-target="#jobs">{{ ui.icon("archive") }}Создать бэкап</button>
<button type="button" class="btn secondary" hx-post="/ui/refresh" hx-include="[name=device_ids]:checked, #dev-filters" hx-target="#devices">{{ ui.icon("refresh") }}Обновить статус</button>
<details class="menu">
<summary class="btn secondary">Обновление {{ ui.icon("chev", 14) }}</summary>
<div class="menu-panel w330">
<button type="button" class="mi warn" hx-post="/ui/batch/ros_update" hx-include="[name=device_ids]:checked" hx-target="#jobs"
hx-confirm="Обновить ROS на выбранных устройствах? Они перезагрузятся после скачивания.">Обновить ROS до актуальной версии<small>Устройство перезагрузится после скачивания</small></button>
<button type="button" class="mi bad" hx-get="/ui/dialog/downgrade" hx-include="[name=device_ids]:checked" hx-target="#modal-body">Откатить ROS до версии канала…<small>Обязателен бэкап; устройство перезагрузится</small></button>
<button type="button" class="mi warn" hx-post="/ui/batch/fw_update" hx-include="[name=device_ids]:checked" hx-target="#jobs"
hx-confirm="Обновить прошивку на выбранных устройствах? Они перезагрузятся сразу после обновления.">Обновить прошивку (FW)<small>Устройство перезагрузится сразу после обновления</small></button>
<button type="button" class="mi bad" hx-get="/ui/dialog/downgrade?kind=fw" hx-include="[name=device_ids]:checked" hx-target="#modal-body">Откатить прошивку до версии ROS…<small>Обязателен бэкап; устройство перезагрузится</small></button>
</div>
</details>
<details class="menu">
<summary class="btn secondary">Канал {{ ui.icon("chev", 14) }}</summary>
<div class="menu-panel w200">
{% for c in channels %}<button type="button" class="mi" hx-post="/ui/batch/channel" hx-vals='{"channel": "{{ c }}"}' hx-include="[name=device_ids]:checked" hx-target="#jobs">{{ c }}</button>{% endfor %}
</div>
</details>
<details class="menu">
<summary class="btn secondary">В группу {{ ui.icon("chev", 14) }}</summary>
<div class="menu-panel w200">
{% for g in groups %}<button type="submit" form="move-form" name="group_id" value="{{ g.id }}" class="mi">{{ g.name }}</button>{% endfor %}
{% if groups %}<hr>{% endif %}
<button type="submit" form="move-form" name="group_id" value="" class="mi">Без группы</button>
</div>
</details>
<span class="grow"></span>
<button type="button" id="clear-sel" class="btn ghost-n">{{ ui.icon("x") }}Снять выбор</button>
</div>
<div id="devices">{% include "_devices.html" %}</div>
</div>
<div id="jobs" hx-get="/ui/jobs" hx-trigger="every 3s" hx-swap="innerHTML">{% include "_jobs.html" %}</div>
{% endblock %}