Files
ros_control/app/ui/templates/backups.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

71 lines
4.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">Бакет {{ bucket }} · {{ stats.total }} {{ stats.total|plural("файл", "файла", "файлов") }} · {{ stats.size|size }}</div>
</div>
<div class="actions"><a class="btn secondary" href="/backups?{{ request.url.query }}">{{ ui.icon("refresh") }}Обновить список</a></div>
</div>
<div class="card">
<div class="tools">
<form method="get" action="/backups">
<select class="field {{ 'on' if flt.group }}" name="group" onchange="this.form.submit()" aria-label="Группа" style="width:170px">
<option value="">Группа: любая</option>
<option value="none" {{ "selected" if flt.group == "none" }}>Группа: без группы</option>
{% for g in groups %}<option value="{{ g.id }}" {{ "selected" if flt.group == g.id|string }}>Группа: {{ g.name }}</option>{% endfor %}
</select>
<select class="field {{ 'on' if flt.device }}" name="device" onchange="this.form.submit()" aria-label="Устройство" style="width:190px">
<option value="">Устройство: любое</option>
{% for d in devices %}<option value="{{ d.name }}" {{ "selected" if d.name == flt.device }}>Устройство: {{ d.name }}</option>{% endfor %}
</select>
<select class="field {{ 'on' if flt.kind }}" name="kind" onchange="this.form.submit()" aria-label="Тип файла" style="width:150px">
<option value="">Тип: любой</option>
<option value="backup" {{ "selected" if flt.kind == "backup" }}>Тип: .backup</option>
<option value="rsc" {{ "selected" if flt.kind == "rsc" }}>Тип: .rsc</option>
</select>
<label class="datefield">с <input type="date" name="date_from" value="{{ flt.date_from }}" onchange="this.form.submit()" aria-label="Дата с"></label>
<label class="datefield">по <input type="date" name="date_to" value="{{ flt.date_to }}" onchange="this.form.submit()" aria-label="Дата по"></label>
<label class="search">{{ ui.icon("search") }}<input type="search" name="q" value="{{ flt.q }}" placeholder="Имя файла" aria-label="Поиск по имени файла" style="width:220px"></label>
<span class="grow"></span>
{% if flt_active %}<a class="btn ghost" href="/backups">Сбросить ({{ flt_active }})</a>{% endif %}
</form>
</div>
{% if error %}<div class="form-err" role="alert" style="margin:0 16px 16px">Ошибка S3: {{ error }}</div>{% endif %}
<div class="table-wrap">
<table class="grid files">
<colgroup><col style="width:20%"><col style="width:140px"><col style="width:100px"><col><col style="width:100px"><col style="width:170px"><col style="width:104px"></colgroup>
<thead><tr><th>Устройство</th><th>Группа</th><th>Тип</th><th>Файл</th><th>Размер</th><th>Изменён (UTC)</th><th><span class="sr-only">Действия</span></th></tr></thead>
<tbody>
{% for i in items %}
<tr>
<td style="font-weight:600">{{ i.device or "—" }}</td>
<td class="muted">{{ i.group or "—" }}</td>
<td><span class="badge">{{ {"backup": ".backup", "rsc": ".rsc"}.get(i.kind, "—") }}</span></td>
<td class="c-mono" title="{{ i.key }}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ i.key.split("/")[-1] }}</td>
<td class="c-small">{{ i.size|size }}</td>
<td class="c-small">{{ i.last_modified|dt }}</td>
<td class="c-menu">
<a class="btn ghost-n icon" href="/backups/download?key={{ i.key|urlencode }}" aria-label="Скачать {{ i.key.split('/')[-1] }}" title="Скачать">{{ ui.icon("download") }}</a>
<details class="menu right">
<summary class="btn ghost-n icon" aria-label="Действия" title="Действия">{{ ui.icon("more") }}</summary>
<div class="menu-panel w200">
<form method="post" action="/backups/delete" onsubmit="return confirm('Удалить файл из бакета?')">
<input type="hidden" name="key" value="{{ i.key }}"><button class="mi bad">Удалить файл</button>
</form>
</div>
</details>
</td>
</tr>
{% else %}
<tr><td colspan="7" class="empty">Файлов не найдено.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tfoot">Найдено {{ items|length }} из {{ stats.total }} {{ stats.total|plural("файла", "файлов", "файлов") }}</div>
</div>
{% endblock %}