Ревью кодовой базы: docs/reviews/2026-09-27-codebase-review.md.
Корректность и согласованность, пункты 5–7 ревью (docs/changes/018):
- одиночное удаление бэкапа в UI идёт через общий delete_many: пометка
deleted_at и событие backup.deleted, как у группового удаления и API;
- единая система миграций: ручные ALTER из db._migrate перенесены в
migrations.run (при user_version < 1, до замены ID);
- групповая смена канала выполняется фоновыми задачами set_channel;
PUT /api/v1/batch/channel → 202 {"job_ids": [...]} (ломающее изменение
API), меню «Канал» в UI выводит задачи в панель «Задачи».
Тесты: 22 из 22. Стенд проверен на порту 8001 (8000 занят посторонним
процессом), боевые данные не изменены. Ручная проверка UI пользователем
на момент коммита не подтверждена.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
6.1 KiB
HTML
86 lines
6.1 KiB
HTML
{% 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 warn" hx-post="/ui/batch/fw_update" hx-include="[name=device_ids]:checked" hx-target="#jobs"
|
||
hx-confirm="Обновить прошивку на выбранных устройствах? Они перезагрузятся сразу после обновления.">Обновить прошивку (FW)<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 %}
|