Веб-морда: поиск, фильтры, сортировка и пагинация клиентов; раскладка настроек
- список клиентов: поиск по домену и названию, фильтр по статусу (чипы со счётчиками), сортировка по любой колонке, пагинация 10/25/50/100; состояние в URL — отфильтрованный вид можно сохранить ссылкой - размеры последних бэкапов одним запросом вместо запроса на клиента - шапка выровнена по колонке контента; настройки на всю ширину: ntfy и Telegram двумя карточками, на узком экране — друг под другом Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c6942605c1
commit
83832fc734
8 files changed
+626
-179
No files matched your search
@@ -1,15 +1,39 @@
|
||||
{{define "title"}}Клиенты — backup-master{{end}}
|
||||
{{define "content"}}
|
||||
{{$v := .V}}{{$q := .V.Query}}
|
||||
<div class="head">
|
||||
<h1>Клиенты</h1>
|
||||
<h1>Клиенты <span class="muted small">{{$v.Total}}</span></h1>
|
||||
<a class="button primary" href="/clients/new">Добавить клиента</a>
|
||||
</div>
|
||||
{{if .Clients}}
|
||||
{{if $v.Total}}
|
||||
<form method="get" action="/" class="toolbar">
|
||||
<input type="search" name="q" value="{{$q.Q}}" placeholder="Поиск по домену или названию" autofocus>
|
||||
{{if $q.Status}}<input type="hidden" name="status" value="{{$q.Status}}">{{end}}
|
||||
{{if ne $q.Sort "domain"}}<input type="hidden" name="sort" value="{{$q.Sort}}">{{end}}
|
||||
{{with $q.DirParam}}<input type="hidden" name="dir" value="{{.}}">{{end}}
|
||||
<select name="per" onchange="this.form.submit()" title="Строк на странице">
|
||||
{{range $v.PerOpts}}<option value="{{.}}" {{if eq . $q.Per}}selected{{end}}>по {{.}}</option>{{end}}
|
||||
</select>
|
||||
<button>Найти</button>
|
||||
{{if or $q.Q $q.Status}}<a class="button" href="/">Сбросить</a>{{end}}
|
||||
</form>
|
||||
<div class="chips toolbar">
|
||||
{{range $v.Chips}}<a class="chip {{if .Active}}active{{end}}" href="{{.URL}}">{{.Title}} <span class="count">{{.Count}}</span></a>{{end}}
|
||||
</div>
|
||||
|
||||
{{if $v.Rows}}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Домен</th><th>Название</th><th>Статус</th><th>Последний бэкап</th><th class="num">Размер</th><th>Агент на связи</th></tr></thead>
|
||||
<thead><tr>
|
||||
<th><a href="{{$q.SortURL "domain"}}" {{if eq $q.Sort "domain"}}class="sorted"{{end}}>Домен {{$q.SortMark "domain"}}</a></th>
|
||||
<th><a href="{{$q.SortURL "name"}}" {{if eq $q.Sort "name"}}class="sorted"{{end}}>Название {{$q.SortMark "name"}}</a></th>
|
||||
<th><a href="{{$q.SortURL "status"}}" {{if eq $q.Sort "status"}}class="sorted"{{end}}>Статус {{$q.SortMark "status"}}</a></th>
|
||||
<th><a href="{{$q.SortURL "last_backup"}}" {{if eq $q.Sort "last_backup"}}class="sorted"{{end}}>Последний бэкап {{$q.SortMark "last_backup"}}</a></th>
|
||||
<th class="num"><a href="{{$q.SortURL "size"}}" {{if eq $q.Sort "size"}}class="sorted"{{end}}>Размер {{$q.SortMark "size"}}</a></th>
|
||||
<th><a href="{{$q.SortURL "last_seen"}}" {{if eq $q.Sort "last_seen"}}class="sorted"{{end}}>Агент на связи {{$q.SortMark "last_seen"}}</a></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{{range .Clients}}
|
||||
{{range $v.Rows}}
|
||||
<tr>
|
||||
<td><a href="/clients/{{.ID}}">{{.Domain}}</a></td>
|
||||
<td>{{if ne .Name .Domain}}{{.Name}}{{end}}</td>
|
||||
@@ -22,6 +46,19 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pager">
|
||||
<span>{{$v.From}}–{{$v.To}} из {{$v.Found}}{{if ne $v.Found $v.Total}} (всего {{$v.Total}}){{end}}</span>
|
||||
{{if $v.Pages}}
|
||||
<div class="pages">
|
||||
{{if $v.Prev}}<a href="{{$v.Prev}}">←</a>{{end}}
|
||||
{{range $v.Pages}}{{if .Gap}}<span class="gap">…</span>{{else if .Current}}<span class="current">{{.N}}</span>{{else}}<a href="{{.URL}}">{{.N}}</a>{{end}}{{end}}
|
||||
{{if $v.Next}}<a href="{{$v.Next}}">→</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="empty">Ничего не найдено. <a href="/">Сбросить фильтры</a></p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p class="empty">Клиентов пока нет. Добавьте первый — получите команду установки агента.</p>
|
||||
{{end}}
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
<body>
|
||||
{{if ne .Page "login"}}
|
||||
<header class="top">
|
||||
<a class="brand" href="/">backup-master</a>
|
||||
<nav>
|
||||
<a href="/" {{if or (eq .Page "clients") (eq .Page "client") (eq .Page "backup")}}class="active"{{end}}>Клиенты</a>
|
||||
<a href="/settings" {{if eq .Page "settings"}}class="active"{{end}}>Настройки</a>
|
||||
<form method="post" action="/logout" class="inline">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<button class="link">Выйти</button>
|
||||
</form>
|
||||
</nav>
|
||||
<div class="wrap">
|
||||
<a class="brand" href="/">backup-master</a>
|
||||
<nav>
|
||||
<a href="/" {{if or (eq .Page "clients") (eq .Page "client") (eq .Page "backup")}}class="active"{{end}}>Клиенты</a>
|
||||
<a href="/settings" {{if eq .Page "settings"}}class="active"{{end}}>Настройки</a>
|
||||
<form method="post" action="/logout" class="inline">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<button class="link">Выйти</button>
|
||||
</form>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
{{end}}
|
||||
<main>
|
||||
|
||||
@@ -13,29 +13,39 @@
|
||||
<p class="error">Ни один канал уведомлений не настроен.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<form method="post" action="/settings" class="card narrow">
|
||||
<form method="post" action="/settings" class="settings">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
|
||||
<h2>ntfy</h2>
|
||||
<label>URL топика <input name="ntfy_url" value="{{.NtfyURL}}" placeholder="https://ntfy.example.com/backups"></label>
|
||||
<label>Авторизация <input name="ntfy_auth" type="password" autocomplete="off" placeholder="{{if .NtfyAuthSet}}задана — оставьте пустым, чтобы не менять{{else}}tk_... или user:password{{end}}"></label>
|
||||
{{if .NtfyAuthSet}}<label class="check"><input type="checkbox" name="ntfy_auth_clear" value="1"> Удалить авторизацию</label>{{end}}
|
||||
<p class="hint">Пустой URL — ntfy выключен.</p>
|
||||
<div class="cards">
|
||||
<section class="card">
|
||||
<h2>ntfy</h2>
|
||||
<label>URL топика <input name="ntfy_url" value="{{.NtfyURL}}" placeholder="https://ntfy.example.com/backups"></label>
|
||||
<label>Авторизация <input name="ntfy_auth" type="password" autocomplete="off" placeholder="{{if .NtfyAuthSet}}задана — оставьте пустым, чтобы не менять{{else}}tk_... или user:password{{end}}"></label>
|
||||
{{if .NtfyAuthSet}}<label class="check"><input type="checkbox" name="ntfy_auth_clear" value="1"> Удалить авторизацию</label>{{end}}
|
||||
<p class="hint">Пустой URL — ntfy выключен.</p>
|
||||
</section>
|
||||
|
||||
<h2>Telegram</h2>
|
||||
<label>Токен бота <input name="tg_token" type="password" autocomplete="off" placeholder="{{if .TGTokenSet}}задан — оставьте пустым, чтобы не менять{{else}}123456:ABC... от @BotFather{{end}}"></label>
|
||||
{{if .TGTokenSet}}<label class="check"><input type="checkbox" name="tg_token_clear" value="1"> Удалить токен (выключить Telegram)</label>{{end}}
|
||||
<label>Chat ID <input name="tg_chats" value="{{.TGChats}}" placeholder="123456789, -1001234567890"></label>
|
||||
<p class="hint">Можно несколько через запятую. Личный чат — ваш числовой id (напишите боту и откройте <code>https://api.telegram.org/bot<токен>/getUpdates</code>), группа — id вида <code>-100…</code>, бот должен быть её участником.</p>
|
||||
<label>ID темы (необязательно) <input name="tg_thread" value="{{.TGThread}}" placeholder="для группы с темами"></label>
|
||||
<label>Адрес Bot API (необязательно) <input name="tg_api_url" value="{{.TGAPIURL}}" placeholder="https://api.telegram.org"></label>
|
||||
<p class="hint">Свой Bot API сервер или прокси, если api.telegram.org недоступен с сервера бэкапов.</p>
|
||||
<section class="card">
|
||||
<h2>Telegram</h2>
|
||||
<label>Токен бота <input name="tg_token" type="password" autocomplete="off" placeholder="{{if .TGTokenSet}}задан — оставьте пустым, чтобы не менять{{else}}123456:ABC... от @BotFather{{end}}"></label>
|
||||
{{if .TGTokenSet}}<label class="check"><input type="checkbox" name="tg_token_clear" value="1"> Удалить токен (выключить Telegram)</label>{{end}}
|
||||
<label>Chat ID <input name="tg_chats" value="{{.TGChats}}" placeholder="123456789, -1001234567890"></label>
|
||||
<p class="hint">Можно несколько через запятую. Личный чат — ваш числовой id (напишите боту и откройте <code>https://api.telegram.org/bot<токен>/getUpdates</code>), группа — id вида <code>-100…</code>, бот должен быть её участником.</p>
|
||||
<div class="grid">
|
||||
<label>ID темы <input name="tg_thread" value="{{.TGThread}}" placeholder="необязательно"></label>
|
||||
<label>Адрес Bot API <input name="tg_api_url" value="{{.TGAPIURL}}" placeholder="https://api.telegram.org"></label>
|
||||
</div>
|
||||
<p class="hint">Тема — для группы с темами. Свой Bot API сервер или прокси — если api.telegram.org недоступен с сервера бэкапов.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h2>Что отправлять</h2>
|
||||
<label class="check"><input type="checkbox" name="notify_success" value="1" {{if .NotifySuccess}}checked{{end}}> Сообщать и об успешных бэкапах (в Telegram — без звука)</label>
|
||||
<p class="hint">Ошибки бэкапа и отсутствие бэкапа дольше заданного у клиента срока приходят всегда, во все настроенные каналы.</p>
|
||||
<label class="check"><input type="checkbox" name="test" value="1"> Отправить тестовое уведомление после сохранения</label>
|
||||
<button class="primary">Сохранить</button>
|
||||
<section class="card">
|
||||
<h2>Что отправлять</h2>
|
||||
<label class="check"><input type="checkbox" name="notify_success" value="1" {{if .NotifySuccess}}checked{{end}}> Сообщать и об успешных бэкапах (в Telegram — без звука)</label>
|
||||
<p class="hint">Ошибки бэкапа и отсутствие бэкапа дольше заданного у клиента срока приходят всегда, во все настроенные каналы.</p>
|
||||
<label class="check"><input type="checkbox" name="test" value="1"> Отправить тестовое уведомление после сохранения</label>
|
||||
<button class="primary">Сохранить</button>
|
||||
</section>
|
||||
</form>
|
||||
<p class="hint">Логин и пароль администратора меняются на сервере: <code>backupctl admin-passwd <логин></code></p>
|
||||
{{end}}
|
||||
Reference in new issue
Block a user