Files
backup-master/internal/server/templates/client.html
T
mrleo1nidandClaude Opus 5.5 1d44d067e9 backup-master: сервер бэкапов, агент и восстановление VPS
Один бинарник backupctl:
- server: API для агентов и restore, веб-морда (один админ), SQLite,
  ретеншн GFS, heavy-части, уведомления ntfy, раздача agent.sh/restore.sh;
- backup/plan: сбор по каталогу категорий (files/systemd/xui/compose),
  горячие дампы Postgres/Mongo/MySQL, снимок x-ui.db, docker-тома,
  digest образов, проверка crontab, потоковая отправка tar.zst;
- restore: провижининг на основе vps_installer с теми же версиями пакетов,
  recipes 3x-ui/nebula/sing-box нужных версий, восстановление compose
  с заливкой дампов и закреплением образов по digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:52:40 +03:00

133 lines
7.3 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>
{{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}}{{join .Paths ", "}}{{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="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}}