Карточка: удерживать скролл боковых блоков при автообновлении; DEPO: «Снять с очереди»

Скролл: автообновление карточки (bjReloadKeepScroll) раньше сохраняло только прокрутку окна,
а боковой «Путь заявки» (.claim-tl — position:fixed, свой overflow) и другие прокручиваемые
блоки (.settings-nav, .doc-toc) отбрасывало наверх. Теперь сохраняется и восстанавливается и
их scrollTop (JSON в sessionStorage, восстановление на rAF/timeout/load).

DEPO: на «Обмен с DEPO» рядом с «Вернуть в очередь» добавлена «Снять с очереди» — помечает
застрявшие записи STATE=SKIPPED (QHl04-06, depoDismissFailures), обработчик их больше не берёт
и они уходят из списка ошибок. Для устаревших/ненужных записей (напр. старые тестовые).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fontvielle
2026-10-13 13:15:34 +03:00
parent 23dd4b43bc
commit 2d2fbb6732
6 changed files with 104 additions and 9 deletions
+40
View File
@@ -120,6 +120,46 @@ func depoRequeueFailures(ctx context.Context, db *sql.DB, days int) (orders, eve
return orders, events, nil return orders, events, nil
} }
// depoDismissFailures — снять записи с ошибкой (за days дней) с очереди: STATE='SKIPPED'.
// Обработчик их больше не берёт, и из списка ошибок они пропадают. Для устаревших/ненужных
// записей, которые не нужно переобрабатывать.
func depoDismissFailures(ctx context.Context, db *sql.DB, days int) (orders, events int64, err error) {
fails, err := depoJobFailures(ctx, db, days, 500)
if err != nil {
return 0, 0, err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return 0, 0, err
}
const comment = "снято с очереди администратором (bj)"
for _, f := range fails {
q := fsql.QHl04
if f.Table == "FTV_M2M_EVENTS" {
q = fsql.QHl05
}
res, e := tx.ExecContext(ctx, q, sql.Named("c", comment), sql.Named("id", f.ID))
if e != nil {
tx.Rollback()
return 0, 0, e
}
n, _ := res.RowsAffected()
if f.Table == "FTV_M2M" {
orders += n
if _, e := tx.ExecContext(ctx, fsql.QHl06, sql.Named("g", f.GUID)); e != nil {
tx.Rollback()
return 0, 0, e
}
} else {
events += n
}
}
if err := tx.Commit(); err != nil {
return 0, 0, err
}
return orders, events, nil
}
var depoFailAlertedID int64 var depoFailAlertedID int64
// depoAlertEventFailures — оповещение о новых ошибках обработки событий BJ_* (поручения с // depoAlertEventFailures — оповещение о новых ошибках обработки событий BJ_* (поручения с
+27
View File
@@ -302,6 +302,33 @@ func (h *setupHandlers) fancyQueueRequeue(w http.ResponseWriter, r *http.Request
http.Redirect(w, r, "/admin/fancy-queue?flash="+url.QueryEscape(msg), http.StatusSeeOther) http.Redirect(w, r, "/admin/fancy-queue?flash="+url.QueryEscape(msg), http.StatusSeeOther)
} }
// fancyQueueDismiss — POST /admin/fancy-queue/dismiss: снять записи с ошибкой с очереди
// (STATE='SKIPPED'). Для устаревших/ненужных записей, которые не нужно переобрабатывать:
// обработчик их больше не берёт и из списка ошибок они пропадают.
func (h *setupHandlers) fancyQueueDismiss(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
msg := ""
db, err := depoEventsDB(h.rc)
if err == nil {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
var o, e int64
o, e, err = depoDismissFailures(ctx, db, 7)
cancel()
db.Close()
if err == nil {
msg = fmt.Sprintf("Снято с очереди: поручений %d, событий %d. Обработчик их больше не возьмёт, из списка ошибок они убраны.", o, e)
log.Printf("depo-events: записи с ошибкой сняты с очереди: поручений %d, событий %d", o, e)
}
}
if err != nil {
msg = "Не удалось снять записи с очереди: " + quikErrorDetail(err)
}
http.Redirect(w, r, "/admin/fancy-queue?flash="+url.QueryEscape(msg), http.StatusSeeOther)
}
func depoJobModeRu(m string) string { func depoJobModeRu(m string) string {
switch m { switch m {
case "bj": case "bj":
+1
View File
@@ -28,6 +28,7 @@ func (h *setupHandlers) registerFancy(mux *http.ServeMux) {
mux.HandleFunc("/admin/setup/fancy/depo-job/script.sql", h.fancyDepoJobScript) mux.HandleFunc("/admin/setup/fancy/depo-job/script.sql", h.fancyDepoJobScript)
mux.HandleFunc("/admin/fancy-queue", h.fancyQueue) mux.HandleFunc("/admin/fancy-queue", h.fancyQueue)
mux.HandleFunc("/admin/fancy-queue/requeue", h.fancyQueueRequeue) mux.HandleFunc("/admin/fancy-queue/requeue", h.fancyQueueRequeue)
mux.HandleFunc("/admin/fancy-queue/dismiss", h.fancyQueueDismiss)
} }
// fancyFromForm собирает настройки из формы (пароль пустой = оставить прежний). // fancyFromForm собирает настройки из формы (пароль пустой = оставить прежний).
+8
View File
@@ -5,3 +5,11 @@ var QHl01 = "UPDATE dbo.FTV_M2M SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL, CO
var QHl02 = "UPDATE dbo.FTV_M2M_EVENTS SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL, COMMENT=@c WHERE ID=@id AND STATE='ERROR'" var QHl02 = "UPDATE dbo.FTV_M2M_EVENTS SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL, COMMENT=@c WHERE ID=@id AND STATE='ERROR'"
var QHl03 = "UPDATE dbo.FTV_M2M_LINES SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL WHERE GUID=@g AND STATE='ERROR'" var QHl03 = "UPDATE dbo.FTV_M2M_LINES SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL WHERE GUID=@g AND STATE='ERROR'"
// Снять с очереди (устаревшие/ненужные записи): STATE='SKIPPED' — обработчик их больше не
// берёт (берёт только TODO) и из списка ошибок (ERROR) они пропадают.
var QHl04 = "UPDATE dbo.FTV_M2M SET STATE='SKIPPED', TimeEnd=GETDATE(), COMMENT=@c WHERE ID=@id AND STATE='ERROR'"
var QHl05 = "UPDATE dbo.FTV_M2M_EVENTS SET STATE='SKIPPED', TimeEnd=GETDATE(), COMMENT=@c WHERE ID=@id AND STATE='ERROR'"
var QHl06 = "UPDATE dbo.FTV_M2M_LINES SET STATE='SKIPPED', TimeEnd=GETDATE() WHERE GUID=@g AND STATE='ERROR'"
@@ -38,7 +38,11 @@
<td style="white-space:nowrap">{{if not .At.IsZero}}{{wall .At}}{{end}}</td><td style="color:var(--err);font-size:12px">{{.Comment}}</td> <td style="white-space:nowrap">{{if not .At.IsZero}}{{wall .At}}{{end}}</td><td style="color:var(--err);font-size:12px">{{.Comment}}</td>
</tr>{{end}}</tbody> </tr>{{end}}</tbody>
</table></div> </table></div>
<form method="post" action="/admin/fancy-queue/requeue" style="margin-top:12px"><button type="submit" class="btn btn-ok" onclick="return confirm('Вернуть записи с ошибкой в очередь обработчика DEPO?')">↻ Вернуть в очередь</button></form> <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px">
<form method="post" action="/admin/fancy-queue/requeue" style="margin:0"><button type="submit" class="btn btn-ok" onclick="return confirm('Вернуть записи с ошибкой в очередь обработчика DEPO? Он попробует обработать их заново.')">↻ Вернуть в очередь</button></form>
<form method="post" action="/admin/fancy-queue/dismiss" style="margin:0"><button type="submit" class="btn" onclick="return confirm('Снять эти записи с очереди? Обработчик больше не будет их брать, и они исчезнут из списка ошибок. Применяйте для устаревших/ненужных записей.')">✕ Снять с очереди</button></form>
</div>
<p class="muted" style="margin:8px 0 0;font-size:12px">«Вернуть в очередь» — повторить обработку после устранения причины. «Снять с очереди» — пометить записи как снятые (SKIPPED) и убрать из ошибок, если переобрабатывать их не нужно (например, старые тестовые).</p>
{{end}} {{end}}
</div>{{end}} </div>{{end}}
+23 -8
View File
@@ -841,18 +841,33 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd
document.addEventListener('change', function(e){ if (e.target.closest('main form')) dirty = true; }, true); document.addEventListener('change', function(e){ if (e.target.closest('main form')) dirty = true; }, true);
try { var u = new URL(location.href); if (u.searchParams.has('flash')) { u.searchParams.delete('flash'); history.replaceState(null, '', u.pathname + u.search + u.hash); } } catch(e){} try { var u = new URL(location.href); if (u.searchParams.has('flash')) { u.searchParams.delete('flash'); history.replaceState(null, '', u.pathname + u.search + u.hash); } } catch(e){}
// Автообновление карточки перерисовывает страницу через перезагрузку. Сохраняем позицию // Автообновление карточки перерисовывает страницу через перезагрузку. Сохраняем позицию
// прокрутки, чтобы при чтении заявку не отбрасывало наверх (автообновление остаётся). // прокрутки окна И внутренних блоков со своим скроллом (боковой «Путь заявки» .claim-tl,
// навигация настроек и т.п.), чтобы при чтении ничего не отбрасывало наверх.
var scrollKey = 'bj-scroll:' + location.pathname; var scrollKey = 'bj-scroll:' + location.pathname;
function bjReloadKeepScroll(){ try { sessionStorage.setItem(scrollKey, String(window.scrollY||window.pageYOffset||0)); } catch(e){} location.reload(); } var keepScrollSel = ['.claim-tl', '.settings-nav', '.doc-toc']; // блоки с собственным скроллом
function bjReloadKeepScroll(){
try {
var st = { w: window.scrollY || window.pageYOffset || 0, el: {} };
keepScrollSel.forEach(function(sel){ var e = document.querySelector(sel); if (e && e.scrollTop) st.el[sel] = e.scrollTop; });
sessionStorage.setItem(scrollKey, JSON.stringify(st));
} catch(e){}
location.reload();
}
try { try {
var sy = sessionStorage.getItem(scrollKey); var raw = sessionStorage.getItem(scrollKey);
if (sy !== null) { if (raw !== null) {
sessionStorage.removeItem(scrollKey); sessionStorage.removeItem(scrollKey);
var syY = parseInt(sy, 10) || 0; var st = JSON.parse(raw); if (!st || typeof st !== 'object') st = {};
var wy = st.w || 0, els = st.el || {};
if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e){} } if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e){} }
window.scrollTo(0, syY); var restore = function(){
setTimeout(function(){ window.scrollTo(0, syY); }, 0); window.scrollTo(0, wy);
window.addEventListener('load', function(){ window.scrollTo(0, syY); }); Object.keys(els).forEach(function(sel){ var e = document.querySelector(sel); if (e) e.scrollTop = els[sel]; });
};
restore();
if (window.requestAnimationFrame) requestAnimationFrame(restore);
setTimeout(restore, 0);
window.addEventListener('load', restore);
} }
} catch(e){} } catch(e){}
function hhmmss(){ var d = new Date(); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2); } function hhmmss(){ var d = new Date(); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2); }