Files
ros_control/app/ui/templates/_devices.html
T
ayurishchevandClaude Sonnet 5 4c1841b61b ros_control: централизованное управление парком MikroTik RouterOS
Control Server (FastAPI) с WEB UI (Jinja2 + HTMX) и JSON API для группового
администрирования устройств RouterOS 7 через REST.

- Устройства: список, статус (модель, канал, версии ROS/FW, uptime, доступные
  обновления), примечания, пароли шифруются (Fernet); фоновый опрос каждые 30 с
  и быстрое обнаружение недоступности (таймаут соединения 4 с).
- Группы устройств и фильтры (устройства, резервные копии).
- Резервные копии: .backup и .rsc (show-sensitive) создаются через REST,
  скачиваются сервером и загружаются в S3 (Yandex Object Storage).
- Обновление ROS/FW и выбор канала, групповые операции задачами.
- Интерфейс по утверждённому макету: светлая/тёмная темы, окна, шрифты IBM Plex
  локально. Docker Compose, SQLite в томе, минимальный набор тестов.
- Планы и итоги каждого изменения — в docs/changes/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 13:13:29 +03:00

76 lines
6.0 KiB
HTML

{% import "_ui.html" as ui %}
{% if oob %}<div class="sub" id="dev-sub" hx-swap-oob="true">{% include "_dev_sub.html" %}</div>{% endif %}
<div class="table-wrap">
<table class="grid">
<colgroup><col style="width:44px"><col><col style="width:110px"><col style="width:110px"><col style="width:100px"><col style="width:120px"><col style="width:90px"><col style="width:120px"><col style="width:100px"><col style="width:150px"><col style="width:52px"></colgroup>
<thead>
<tr>
<th><input type="checkbox" id="check-all" aria-label="Выбрать все"></th>
<th>Устройство</th><th>Группа</th><th>Канал</th><th>ROS</th><th>Upgrade ROS</th><th>FW</th><th>Upgrade FW</th><th>Uptime</th><th>Запрос бэкапа (UTC)</th><th><span class="sr-only">Действия</span></th>
</tr>
</thead>
<tbody>
{% for d in devices %}
{% set st = d.status %}{% set down = d.online is false %}
{% set ros_new = st.ros_latest and st.ros_installed and st.ros_latest != st.ros_installed %}
{% set fw_new = st.fw_current and st.fw_upgrade and st.fw_upgrade != st.fw_current %}
<tr>
<td><input type="checkbox" name="device_ids" value="{{ d.id }}" form="move-form" aria-label="Выбрать {{ d.name }}"></td>
<td>
<div class="dev">
<span class="dot {{ 'on' if d.online else ('off' if down else '') }}" title="{{ d.last_error or ('online, опрошено ' ~ (d.status_at|dt)) }}"></span>
<div style="min-width:0">
<div class="dev-name"><a class="dev-link" href="/devices/{{ d.id }}/edit" hx-get="/ui/dialog/device/{{ d.id }}" hx-target="#modal-body"
title="{{ d.note or 'Изменить устройство' }}">{{ d.name }}</a></div>
{% if down %}<div class="dev-sub bad" title="{{ d.last_error }}">Нет соединения</div>
{% else %}<div class="dev-sub">{{ d.host }}:{{ d.port }}{% if st.model %} · {{ st.model }}{% endif %}</div>{% endif %}
</div>
</div>
</td>
<td class="muted">{{ group_names.get(d.group_id) or "—" }}</td>
<td>{% if down %}<span class="dash">—</span>{% else %}{{ st.channel or "—" }}{% endif %}</td>
<td class="c-mono">{% if down %}<span class="dash">—</span>{% else %}{{ st.ros_installed or st.ros_version or "—" }}{% endif %}</td>
<td>
{% if down or not (st.ros_latest and st.ros_installed) %}<span class="dash" title="{{ st.ros_check_error or 'Проверка обновлений ещё не выполнялась' }}">—</span>
{% elif ros_new %}<span class="badge warn" title="Доступно обновление ROS до {{ st.ros_latest }}">{{ ui.icon("up", 12, 2.2) }}{{ st.ros_latest }}</span>
{% else %}<span class="okmark">{{ ui.icon("check", 14, 2.2) }}актуально</span>{% endif %}
</td>
<td class="c-mono">{% if down %}<span class="dash">—</span>{% else %}{{ st.fw_current or "—" }}{% endif %}</td>
<td>
{% if down or not (st.fw_current and st.fw_upgrade) %}<span class="dash" title="Нет RouterBOARD firmware (например, CHR) или устройство не опрошено">—</span>
{% elif fw_new %}<span class="badge warn" title="Доступно обновление прошивки до {{ st.fw_upgrade }}">{{ ui.icon("up", 12, 2.2) }}{{ st.fw_upgrade }}</span>
{% else %}<span class="okmark">{{ ui.icon("check", 14, 2.2) }}актуально</span>{% endif %}
</td>
<td class="c-small">{% if down %}<span class="dash">—</span>{% else %}{{ st.uptime or "—" }}{% endif %}</td>
<td class="c-small">{{ d.last_backup_requested_at|dt }}</td>
<td class="c-menu">
<details class="menu right">
<summary class="btn ghost-n icon" aria-label="Действия для {{ d.name }}" title="Действия">{{ ui.icon("more") }}</summary>
<div class="menu-panel w270">
<button type="button" class="mi" hx-post="/ui/devices/{{ d.id }}/refresh" hx-include="#dev-filters" hx-target="#devices">Обновить статус</button>
<button type="button" class="mi" hx-post="/ui/devices/{{ d.id }}/backup" hx-target="#jobs">Создать бэкап</button>
<hr>
<button type="button" class="mi warn" hx-post="/ui/devices/{{ d.id }}/ros_update" hx-target="#jobs"
hx-confirm="Обновить ROS на {{ d.name }}? Устройство перезагрузится после скачивания.">Обновить ROS<small>Перезагрузка после скачивания</small></button>
<button type="button" class="mi warn" hx-post="/ui/devices/{{ d.id }}/fw_update" hx-target="#jobs"
hx-confirm="Обновить прошивку на {{ d.name }}? Устройство перезагрузится сразу после обновления.">Обновить FW<small>Перезагрузка сразу после обновления</small></button>
<hr>
<details class="sub">
<summary class="mi">Сменить канал…</summary>
<div class="sub-list">
{% for c in channels %}<button type="button" class="mi {{ 'on' if c == st.channel }}" hx-post="/ui/devices/{{ d.id }}/channel" hx-vals='{"channel": "{{ c }}"}' hx-include="#dev-filters" hx-target="#devices">{{ c }}</button>{% endfor %}
</div>
</details>
<button type="button" class="mi" hx-get="/ui/dialog/device/{{ d.id }}" hx-target="#modal-body">Изменить устройство</button>
</div>
</details>
</td>
</tr>
{% else %}
<tr><td colspan="11" class="empty">{{ "Нет устройств, подходящих под фильтр." if filtered else "Устройств пока нет. Добавьте первое." }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tfoot">Показано {{ devices|length }} из {{ total }} {{ total|plural("устройства", "устройств", "устройств") }}</div>