The 5s auto-refresh (ec44d54) and the hx-sync fix on top of it (5a53705) didn't resolve the issues seen in manual testing. Rather than keep debugging htmx's polling/preserve/sync interaction, drop auto-refresh entirely: handleIPsFragment, GET /ips/fragment, and the poll attributes/PollSeconds plumbing are all removed. The table now only updates when a button action re-renders it, as it did before auto-refresh was added — the bulk-recheck feature itself (handleIPsRecheckSelected, POST /ips/recheck, "Перепроверить выбранные") is untouched. Also drops hx-preserve/id from the row checkboxes: it existed solely to survive the auto-poll wiping a selection mid-task, so it has no purpose left, and it was actively wrong for one case — after a successful "Перепроверить выбранные", it kept the just-submitted addresses checked instead of clearing them. Since the checkbox's checked state was never server-rendered to begin with, removing hx-preserve alone makes every table swap (including the recheck button's own) render fresh, unchecked boxes, which is exactly the desired "selection clears once the action has been applied" behavior. Left hx-sync="#ips-table-wrap:queue last" on the action buttons/form — still cheap protection against a double-click race between two real user actions, independent of the now-removed polling. Rebuilt bin/admin-dashboard and bin/SHA256SUMS per docs/SETUP.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
87 lines
4.4 KiB
HTML
87 lines
4.4 KiB
HTML
{{define "ips_page"}}
|
|
<!doctype html>
|
|
<html lang="ru">
|
|
<head>{{template "html_head" .}}</head>
|
|
<body>
|
|
<div class="bg-grid"></div>
|
|
<input type="checkbox" id="nav-toggle" class="nav-toggle">
|
|
<div class="shell">
|
|
{{template "sidebar_nav" .}}
|
|
<div>
|
|
{{template "topbar_mobile" .}}
|
|
<main class="main">
|
|
<div id="error-banner">{{template "banner_inner" .Banner}}</div>
|
|
{{template "ips_content" .}}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
{{end}}
|
|
|
|
{{define "ips_content"}}
|
|
<div class="topbar"><h1>Очередь IP</h1></div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-body">
|
|
<form hx-post="/ips" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last" hx-on::after-request="this.reset()">
|
|
<div class="field-row">
|
|
<div class="field" style="flex: 1 1 420px;">
|
|
<label for="addresses">Адреса (по одному на строку или через запятую)</label>
|
|
<textarea id="addresses" name="addresses" rows="2" placeholder="203.0.113.10 203.0.113.11"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Добавить / перепроверить</button>
|
|
</div>
|
|
</form>
|
|
<p class="muted" style="margin-top:8px">Новый адрес встаёт в очередь; уже завершённый (done/failed) запускается заново
|
|
(тот же вызов); тот, что сейчас проверяется, не трогается.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ips-table-wrap">
|
|
{{template "ips_table" .}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "ips_table"}}
|
|
<form id="ips-form">
|
|
<div class="panel">
|
|
<div class="panel-body" style="display:flex; gap:8px; align-items:center;">
|
|
<button type="button" class="btn btn-ghost btn-sm" hx-post="/ips/recheck" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last">Перепроверить выбранные</button>
|
|
<button type="button" class="btn btn-danger-ghost btn-sm" hx-post="/ips/delete" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last" hx-confirm="Удалить выбранные адреса без возможности восстановления?">Удалить выбранные</button>
|
|
<button type="button" class="btn btn-danger-ghost btn-sm" hx-post="/ips/clear" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last" hx-confirm="Удалить ВСЕ адреса из очереди, включая те, что сейчас проверяются? Действие необратимо.">Очистить всё</button>
|
|
</div>
|
|
<div class="table-scroll">
|
|
<table>
|
|
<thead><tr><th><input type="checkbox" onclick="this.closest('table').querySelectorAll('input[name=addresses]').forEach(cb => cb.checked = this.checked)"></th><th>Адрес</th><th>Состояние</th><th>Валидатор</th><th>Попытка</th><th>Обновлено</th><th></th></tr></thead>
|
|
<tbody>
|
|
{{range .Items}}
|
|
{{$b := ipBadge .State .OverallResult .FIPAssociatedAt $.FIPSettleSeconds}}
|
|
{{$terminal := or (eq .State "done") (eq .State "failed") (eq .State "occupied")}}
|
|
<tr>
|
|
<td data-label=""><input type="checkbox" name="addresses" value="{{.IPAddress}}"></td>
|
|
<td class="addr" data-label="Адрес"><a href="/ips/{{.IPAddress}}">{{.IPAddress}}</a></td>
|
|
<td data-label="Состояние"><span class="pill {{$b.Class}}">{{$b.Label}}</span></td>
|
|
<td data-label="Валидатор">{{deref .OwnerValidatorID}}</td>
|
|
<td class="num" data-label="Попытка">{{.AttemptNumber}}</td>
|
|
<td class="num" data-label="Обновлено">{{fmtTime .UpdatedAt}}</td>
|
|
<td data-label="">
|
|
<div class="actions">
|
|
{{if $terminal}}
|
|
<button class="btn btn-ghost btn-sm" hx-post="/ips/{{.IPAddress}}/recheck" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last">Перепроверить</button>
|
|
{{else}}
|
|
<button class="btn btn-danger-ghost btn-sm" hx-post="/ips/{{.IPAddress}}/cancel" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last" hx-confirm="Остановить проверку {{.IPAddress}}?">Отменить</button>
|
|
{{end}}
|
|
<button class="btn btn-danger-ghost btn-sm" hx-delete="/ips/{{.IPAddress}}" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last" hx-confirm="Удалить {{.IPAddress}} без возможности восстановления?">Удалить</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{{if not .Items}}<p class="muted">Очередь пуста.</p>{{end}}
|
|
{{end}}
|