web-ui: добавлена страница группового удаления загрузок

- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа
  на обеих границах, потолок времени и остановка после трёх подряд отказов
  внешнего сервиса
- допуск полного удаления сведён в единую точку store.State.CanDelete() —
  worker, страница загрузки и Telegram больше не держат своих перечней
This commit is contained in:
av
2026-08-10 17:43:36 +03:00
parent a7c1efd8eb
commit 288be8ec34
35 changed files with 3185 additions and 31 deletions
+17 -7
View File
@@ -48,22 +48,20 @@ func (b *Bot) renderCard(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboar
case store.StateLinking:
return "⏳ Раскладываю #" + idCode(id) + "…", nil
case store.StateDone:
return b.renderDone(rd), b.deletableKeyboard(id)
return b.renderDone(rd), b.stateKeyboard(id, state)
default:
// state — внутренний enum состояния (не внешний ввод), экранировать не нужно.
text := fmt.Sprintf("Задача #%s — %s.", idCode(id), state)
if msg := rd.Download.ErrorMsg.String; msg != "" {
text += "\n" + esc(msg)
}
switch state {
case store.StateFailed, store.StateStuck:
if state == store.StateFailed || state == store.StateStuck {
// failed/stuck — даём кнопку повтора.
return text, b.retryKeyboard(id)
case store.StateOrphaned, store.StateTargetMissing:
// Рассинхрон — можно подчистить остатки полным удалением.
return text, b.deletableKeyboard(id)
}
return text, b.webOnly(id)
// Рассинхрон (orphaned/target_missing) — можно подчистить остатки полным
// удалением; какие именно состояния это допускают, знает домен.
return text, b.stateKeyboard(id, state)
}
}
@@ -292,6 +290,18 @@ func (b *Bot) retryKeyboard(id string) *tgbotapi.InlineKeyboardMarkup {
return &kb
}
// stateKeyboard выбирает клавиатуру по допуску удаления: своего перечня
// состояний транспорт не держит, предикат живёт в домене (см.
// openspec/specs/state-reconciliation, «Полное удаление загрузки
// пользователем»). Допуск в ядре это не заменяет — worker.Delete проверяет его
// сам.
func (b *Bot) stateKeyboard(id string, state store.State) *tgbotapi.InlineKeyboardMarkup {
if state.CanDelete() {
return b.deletableKeyboard(id)
}
return b.webOnly(id)
}
// deletableKeyboard — клавиатура состояний, откуда доступно полное удаление
// (done/orphaned/target_missing): ссылка в веб (опц.) + «Закрыть» (стоп-кран, лишь
// статус) + «Удалить» (снос раздачи+файлов). Обе команды двухшаговые — кнопка