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>
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.isdigit() }}" 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, #dev-filters" hx-target="#devices">{{ 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 %}
|