Агент: - ssh: sshd_config(.d), ключи хоста, authorized_keys root и пользователей - system: весь /etc/ufw и /etc/default/ufw, /etc/sysctl.conf, /etc/hosts, свои файлы в /etc/cron.d (пакетные отсекаются через dpkg -S) - certs: /etc/letsencrypt, /root/.acme.sh; xui: сертификаты по путям из базы - docker: daemon.json и логины в registry; telegraf: /etc/default/telegraf - caddy: бинарник бэкапится, если Caddy собран с плагинами - units: свои systemd-юниты с состоянием enabled/active и drop-in *.service.d - etc: изменённые конфиги любых пакетов (dpkg conffiles) и свои файлы в /etc, кроме привязанного к железу и облаку - в манифесте: часовой пояс, swap, вручную поставленные пакеты; версия apt-пакета в строке категории - предупреждения: программы юнитов вне бэкапа, каталоги /opt и /usr/local/bin без категории, контейнеры без compose Restore: - часовой пояс, swap-файл, доустановка вручную поставленных пакетов - свои юниты включаются и запускаются как на старом сервере Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
7.5 KiB
HTML
134 lines
7.5 KiB
HTML
{{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}}<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}} {{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 < "$DUMP_FILE"">{{.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}}
|