Files
backup-master/internal/server/templates/client.html
T
mrleo1nidandClaude Opus 5.5 488abc2d34
CI / test (push) Successful in 1m29s
Агенты подхватывают изменения с сервера: каталог и самообновление
- сервер отдаёт агенту итоговый каталог (встроенный каталог сервера +
  переопределения клиента); если каталог не подходит версии агента —
  встроенный каталог агента и предупреждение
- самообновление перед бэкапом: версия сервера новее → скачивание с того же
  сервера (только /dl/...), проверка sha256 и запуска новой версии, подмена
  с откатом, перезапуск через exec; при ошибке бэкап идёт текущей версией
- галочка «Автообновление» у клиента (по умолчанию включена), версия агента
  в списке и карточке, отставшие подсвечены; версия сервера в шапке
- миграция существующей базы: колонки auto_update и agent_version
- backupctl backup --no-update

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:27:31 +03:00

136 lines
8.1 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.
{{define "title"}}{{.Client.Domain}} — backup-master{{end}}
{{define "content"}}
{{$csrf := .CSRF}}
<div class="head">
<div>
<h1>{{.Client.Domain}} <span class="badge {{.StatusClass}}">{{.StatusText}}</span></h1>
{{if ne .Client.Name .Client.Domain}}<p class="muted">{{.Client.Name}}</p>{{end}}
</div>
<div class="actions">
<form method="post" action="/clients/{{.Client.ID}}/restore-token" class="inline">
<input type="hidden" name="csrf" value="{{$csrf}}">
<button class="primary" {{if not .Latest}}disabled title="Нет успешных бэкапов"{{end}}>Команда восстановления</button>
</form>
<form method="post" action="/clients/{{.Client.ID}}/token" class="inline" onsubmit="return confirm('Старый токен перестанет работать. Продолжить?')">
<input type="hidden" name="csrf" value="{{$csrf}}">
<button>Новый токен агента</button>
</form>
</div>
</div>
{{with .Error}}<p class="error">{{.}}</p>{{end}}
{{if .Client.LastError}}<p class="error">Последняя ошибка ({{ts .Client.LastErrorAt}}): {{.Client.LastError}}</p>{{end}}
<dl class="facts">
<div><dt>Последний бэкап</dt><dd title="{{ts .Client.LastBackupAt}}">{{ago .Client.LastBackupAt}}</dd></div>
<div><dt>Heavy-части</dt><dd title="{{ts .Client.LastHeavyAt}}">{{ago .Client.LastHeavyAt}}</dd></div>
<div><dt>Агент на связи</dt><dd title="{{ts .Client.LastSeenAt}}">{{ago .Client.LastSeenAt}}</dd></div>
<div><dt>Версия агента</dt><dd>{{with .Client.AgentVersion}}{{.}}{{else}}—{{end}}{{if .AgentOutdated}} <span class="badge stale" title="Сервер {{.ServerVersion}}">{{if .Client.AutoUpdate}}обновится при следующем бэкапе{{else}}устарел, автообновление выключено{{end}}</span>{{end}}</dd></div>
{{with .Latest}}<div><dt>Система</dt><dd>{{.Hostname}}{{with .OS.PrettyName}} · {{.}}{{end}}{{with .OS.Arch}} · {{.}}{{end}}</dd></div>{{end}}
</dl>
{{with .Latest}}
<h2>Что бэкапится <span class="muted small">по последнему бэкапу</span></h2>
<div class="table-wrap">
<table>
<thead><tr><th>Категория</th><th>Тип</th><th>Версия</th><th>Сервис</th><th>Содержимое</th></tr></thead>
<tbody>
{{range .Categories}}
<tr>
<td>{{.Name}}{{if .Auto}} <span class="badge stale" title="Проект найден автоматически и не описан в каталоге — бэкапится с настройками по умолчанию">авто</span>{{end}}</td>
<td>{{.Type}}</td>
<td>{{.Version}}{{with .Compose}}{{with .Git}}<span class="muted small" title="{{.Remote}}">git {{printf "%.8s" .Commit}}{{if .Dirty}}*{{end}}</span>{{end}}{{end}}</td>
<td>{{with .Service}}{{.Unit}} {{if .Active}}<span class="ok-text">active</span>{{else}}<span class="muted">inactive</span>{{end}}{{if not .Enabled}} <span class="muted">(disabled)</span>{{end}}{{end}}</td>
<td class="small">
{{if .Paths}}<span title="{{join .Paths ", "}}">{{short .Paths 8}}</span>{{end}}
{{range .Units}}<br>юнит {{.Unit}}{{if .Enabled}} <span class="muted">enabled</span>{{end}}{{if .Active}} <span class="ok-text">active</span>{{end}}{{end}}
{{range .Dumps}}<br>дамп {{.Kind}}{{with .Service}} ({{.}}){{end}}{{end}}
{{range .Volumes}}<br>том {{.Name}}{{end}}
{{if .Heavy}}<br>heavy: {{join .Heavy ", "}}{{if .HeavySkipped}} <span class="muted">(не в этот раз)</span>{{end}}{{end}}
</td>
</tr>
{{else}}
<tr><td colspan="5" class="muted">Ни одной категории не найдено.</td></tr>
{{end}}
</tbody>
</table>
</div>
{{if .Packages}}
<p class="small muted">Пакеты: {{range $k, $v := .Packages}}{{$k}}={{$v}} &nbsp; {{end}}</p>
{{end}}
{{if .Warnings}}
<details class="warnings" open>
<summary>Предупреждения ({{len .Warnings}})</summary>
<ul>{{range .Warnings}}<li>{{.}}</li>{{end}}</ul>
</details>
{{end}}
{{end}}
<h2>История</h2>
{{if .Backups}}
<div class="table-wrap">
<table>
<thead><tr><th>#</th><th>Дата</th><th>Статус</th><th class="num">Размер</th><th>Части</th><th></th></tr></thead>
<tbody>
{{range .Backups}}
{{$b := .}}
<tr>
<td><a href="/backups/{{.ID}}">{{.ID}}</a></td>
<td>{{ts .CreatedAt}}</td>
<td><span class="badge {{if eq .Status "complete"}}ok{{else if eq .Status "failed"}}fail{{else}}stale{{end}}">{{.Status}}</span>{{with .Error}} <span class="small muted">{{.}}</span>{{end}}</td>
<td class="num">{{if .TotalSize}}{{bytes .TotalSize}}{{end}}</td>
<td class="small">{{range .Parts}}{{if eq .Status "complete"}}<a href="/backups/{{$b.ID}}/parts/{{.Name}}">{{.Name}}</a> ({{bytes .Size}}) {{else if eq .Status "deleted"}}<span class="muted">{{.Name}} удалена</span> {{end}}{{end}}</td>
<td class="right">
<form method="post" action="/backups/{{.ID}}/delete" class="inline" onsubmit="return confirm('Удалить бэкап #{{.ID}}?')">
<input type="hidden" name="csrf" value="{{$csrf}}">
<button class="link danger">удалить</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="empty">Бэкапов ещё нет.</p>
{{end}}
<h2>Настройки</h2>
<form method="post" action="/clients/{{.Client.ID}}/settings" class="card">
<input type="hidden" name="csrf" value="{{$csrf}}">
<label>Название <input name="name" value="{{.Client.Name}}"></label>
<div class="grid">
<label>Дневных копий <input type="number" min="0" name="keep_daily" value="{{.Client.KeepDaily}}"></label>
<label>Недельных <input type="number" min="0" name="keep_weekly" value="{{.Client.KeepWeekly}}"></label>
<label>Месячных <input type="number" min="0" name="keep_monthly" value="{{.Client.KeepMonthly}}"></label>
<label>Тревога через, ч <input type="number" min="0" name="stale_hours" value="{{.Client.StaleHours}}"></label>
<label>Heavy раз в, дней <input type="number" min="0" name="heavy_every_days" value="{{.Client.HeavyEveryDays}}"></label>
<label>Хранить heavy, шт <input type="number" min="0" name="heavy_keep" value="{{.Client.HeavyKeep}}"></label>
</div>
<label class="check"><input type="checkbox" name="auto_update" value="1" {{if .Client.AutoUpdate}}checked{{end}}> Автообновление агента до версии сервера (перед бэкапом)</label>
<label class="check"><input type="checkbox" name="disabled" value="1" {{if .Client.Disabled}}checked{{end}}> Клиент отключён (агент получает отказ)</label>
<label>Переопределения каталога (YAML)
<textarea name="config_yaml" rows="12" spellcheck="false" placeholder="categories:
- {name: scripts, disabled: true} # отключить встроенную категорию
- name: extra # добавить свою
type: files
paths: [/srv/extra]
dumps:
- name: full_dump.sql
cmd: mysqldump --all-databases
restore_cmd: mysql &lt; &quot;$DUMP_FILE&quot;">{{.Client.ConfigYAML}}</textarea>
</label>
<p class="hint">Категория с тем же <code>name</code>, что во встроенном каталоге, заменяет её целиком. Поля: type (files|systemd|xui|compose), paths, exclude, detect, service, version_cmd, install, keep_binaries, ensure_dirs, post_restore, dumps, dir, skip_services, heavy_paths, restore_after.</p>
<button class="primary">Сохранить</button>
</form>
<details class="danger-zone">
<summary>Удаление клиента</summary>
<form method="post" action="/clients/{{.Client.ID}}/delete">
<input type="hidden" name="csrf" value="{{$csrf}}">
<p>Будут удалены все бэкапы клиента. Введите домен для подтверждения:</p>
<input name="confirm" placeholder="{{.Client.Domain}}">
<button class="danger">Удалить клиента и бэкапы</button>
</form>
</details>
{{end}}