web-ui: добавлена страница группового удаления загрузок
- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа на обеих границах, потолок времени и остановка после трёх подряд отказов внешнего сервиса - допуск полного удаления сведён в единую точку store.State.CanDelete() — worker, страница загрузки и Telegram больше не держат своих перечней
This commit is contained in:
@@ -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): ссылка в веб (опц.) + «Закрыть» (стоп-кран, лишь
|
||||
// статус) + «Удалить» (снос раздачи+файлов). Обе команды двухшаговые — кнопка
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
)
|
||||
|
||||
// Кнопка «Удалить» приходит ровно в тех состояниях, где домен допускает полное
|
||||
// удаление. Транспорт своего перечня состояний не держит: разъехавшись с
|
||||
// доменом, он показал бы кнопку, ведущую в конфликт, — или спрятал бы
|
||||
// доступное действие.
|
||||
func TestRenderCard_DeleteButtonFollowsCanDelete(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
|
||||
all := []store.State{
|
||||
store.StateCatched, store.StateDownloading, store.StateCompleted,
|
||||
store.StateRecognizing, store.StateReview, store.StateLinking,
|
||||
store.StateDone, store.StateDeferred, store.StateStuck,
|
||||
store.StateFailed, store.StateCancelled, store.StateReverted,
|
||||
store.StateTargetMissing, store.StateOrphaned, store.StateDeleted,
|
||||
}
|
||||
|
||||
for _, st := range all {
|
||||
rd := reviewData(st)
|
||||
_, kb := b.renderCard(rd)
|
||||
|
||||
found := false
|
||||
if kb != nil {
|
||||
for _, row := range kb.InlineKeyboard {
|
||||
for _, btn := range row {
|
||||
if btn.CallbackData != nil && strings.HasPrefix(*btn.CallbackData, "delete:") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if want := st.CanDelete(); found != want {
|
||||
t.Errorf("%s: кнопка удаления=%v, CanDelete=%v", st, found, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user