Глобально уникальные ID (docs/changes/016): - у устройств, групп, задач, резервных копий и записей журнала ID вида <префикс>_<uuid7> (dev_, grp_, job_, bkp_, evt_): типы не пересекаются, внутри типа ID не повторяются и сортируются по времени; - миграция БД v1 заменяет числовые ID с пересчётом ссылок (копия файла БД перед миграцией, сверка числа строк, одна транзакция); - резервная копия = пара файлов с одним bkp_ ID, файлы в S3 получают метаданные backup-id/device-id; синхронизация метаданных с бакетом; - журнал событий в БД (events): создание/изменение/удаление, задачи, бэкапы, смена online/offline, вход в UI; API чтения GET /api/v1/events. Журнал в UI, ротация и очистка (docs/changes/017): - страница «Журнал»: фильтры, подгрузка «Показать ещё», окно записи; - настройки ротации (срок и максимум записей) хранятся в БД (схема v2), ротация при старте, раз в час и после сохранения настроек; - очистка журнала только через окно с паролем пользователя, блокировка после 5 неверных попыток, остаётся запись о факте очистки; через API очистки нет. Тесты: 20 из 20. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
4.8 KiB
HTML
53 lines
4.8 KiB
HTML
{# Форма устройства: одна разметка для окна (in_modal) и запасной страницы. v — значения полей. #}
|
|
{% import "_ui.html" as ui %}
|
|
{% set edit = device is not none %}
|
|
{% set action = ('/devices/' ~ device.id ~ '/edit') if edit else '/devices/new' %}
|
|
<form method="post" action="{{ action }}" {% if in_modal %}hx-post="{{ action }}" hx-target="#modal-body"{% endif %}>
|
|
<header class="modal-head">
|
|
<h2 id="modal-title">{{ "Изменить устройство" if edit else "Новое устройство" }}</h2>
|
|
{% if in_modal %}<button type="button" class="btn ghost-n icon" data-close aria-label="Закрыть">{{ ui.icon("x") }}</button>{% endif %}
|
|
</header>
|
|
<div class="modal-body">
|
|
{% if error %}<div class="form-err" role="alert">{{ error }}</div>{% endif %}
|
|
<label class="lbl"><span>Название <small>{% if edit %}задаётся при создании и не меняется — входит в ключи бэкапов{% else %}латиница, цифры, «.», «_», «-»; попадает в ключи бэкапов{% endif %}</small></span>
|
|
{% if edit %}<input class="input" value="{{ v.name }}" readonly aria-readonly="true" tabindex="-1">
|
|
{% else %}<input class="input" name="name" value="{{ v.name }}" maxlength="64" required autofocus>{% endif %}</label>
|
|
<label class="lbl"><span>Примечание <small>необязательно</small></span>
|
|
<textarea class="input" name="note" rows="3" maxlength="500" placeholder="Например: серверная, 2 этаж; ответственный — дежурный инженер">{{ v.note }}</textarea></label>
|
|
<div class="form-row host">
|
|
<label class="lbl">Адрес<input class="input" name="host" value="{{ v.host }}" required></label>
|
|
<label class="lbl">Порт<input class="input" name="port" type="number" min="1" max="65535" value="{{ v.port }}" required></label>
|
|
</div>
|
|
<div class="form-row">
|
|
<label class="lbl">Пользователь<input class="input" name="username" value="{{ v.username }}" required></label>
|
|
<label class="lbl"><span>Пароль {% if edit %}<small>пусто — не менять</small>{% endif %}</span>
|
|
<input class="input" name="password" type="password" autocomplete="new-password" {{ "" if edit else "required" }}></label>
|
|
</div>
|
|
<label class="lbl">Группа
|
|
<select class="field" name="group_id" data-newgroup style="width:100%">
|
|
<option value="">— без группы —</option>
|
|
{% for g in groups %}<option value="{{ g.id }}" {{ "selected" if v.group_id == g.id }}>{{ g.name }}</option>{% endfor %}
|
|
<option value="__new__" {{ "selected" if v.group_id == "__new__" }}>+ Новая группа…</option>
|
|
</select>
|
|
</label>
|
|
<label class="lbl" id="new-group-field" {{ "" if v.group_id == "__new__" else "hidden" }}>Название новой группы
|
|
<input class="input" name="new_group" value="{{ v.new_group }}" maxlength="64"></label>
|
|
<label class="check"><input name="use_tls" type="checkbox" {{ "checked" if v.use_tls }}>
|
|
<span>HTTPS (сервис www-ssl)<small>Порт обычно 443; без HTTPS — сервис www, порт 80</small></span></label>
|
|
<div class="note" id="http-note" {{ "hidden" if v.use_tls }}>Без HTTPS логин и пароль передаются по сети открытым текстом — только для доверенной сети.</div>
|
|
<label class="check"><input name="verify_tls" type="checkbox" {{ "checked" if v.verify_tls }}>
|
|
<span>Проверять TLS-сертификат<small>Отключите для самоподписанных сертификатов</small></span></label>
|
|
</div>
|
|
<footer class="modal-foot">
|
|
{% if edit %}
|
|
{% if in_modal %}<button type="button" class="btn danger" hx-post="/devices/{{ device.id }}/delete"
|
|
hx-confirm="Удалить устройство {{ device.name }} из списка? Бэкапы в бакете сохранятся.">Удалить устройство</button>
|
|
{% else %}<button class="btn danger" formaction="/devices/{{ device.id }}/delete" formnovalidate
|
|
onclick="return confirm('Удалить устройство {{ device.name }} из списка? Бэкапы в бакете сохранятся.')">Удалить устройство</button>{% endif %}
|
|
{% endif %}
|
|
<span class="grow"></span>
|
|
{% if in_modal %}<button type="button" class="btn ghost-n" data-close>Отмена</button>{% else %}<a class="btn ghost-n" href="/">Отмена</a>{% endif %}
|
|
<button class="btn primary">{{ "Сохранить" if edit else "Добавить устройство" }}</button>
|
|
</footer>
|
|
</form>
|