From a0f6ce9f7b5a13e270e482c1b66cb856dd4888b5 Mon Sep 17 00:00:00 2001 From: zuevav Date: Fri, 25 Sep 2026 16:07:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BE=D0=BA=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D1=8F=D1=8E=D1=82=D1=81=D1=8F=20=D1=81?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8:=20=D0=BE=D1=82=D0=BF=D0=B5=D1=87=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F,=20=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B8=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B1=D0=B5=D0=B7=20=D1=80=D1=83?= =?UTF-8?q?=D1=87=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F,=20=D0=B8=D0=BD=D0=B4=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=BE=D1=80=20=C2=AB=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi --- internal/lkgateway/livestatus.go | 52 +++++++++++++++++-- .../lkgateway/web/templates/admin_claim.html | 2 +- .../web/templates/admin_incoming_detail.html | 2 +- internal/lkgateway/web/templates/layout.html | 37 ++++++++++++- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/internal/lkgateway/livestatus.go b/internal/lkgateway/livestatus.go index e5ca730..4036de4 100644 --- a/internal/lkgateway/livestatus.go +++ b/internal/lkgateway/livestatus.go @@ -11,7 +11,9 @@ package lkgateway import ( "context" "encoding/json" + "hash/fnv" "net/http" + "strconv" "strings" "sync" "time" @@ -22,6 +24,48 @@ type LiveStatus struct { Text string `json:"text"` // чего ждём / чем закончилось Kind string `json:"kind"` // wait | warn | ok | bad Active bool `json:"active"` // в работе: страница продолжает опрашивать + Sig string `json:"sig,omitempty"` // отпечаток состояния: изменился — карточка перерисовывается +} + +// liveSig — короткий отпечаток всего, что показывает карточка: сменился — страница обновляется. +func liveSig(parts ...any) string { + raw, _ := json.Marshal(parts) + h := fnv.New64a() + _, _ = h.Write(raw) + return strconv.FormatUint(h.Sum64(), 36) +} + +// liveOutgoing — состояние исходящего перевода с отпечатком карточки. +func liveOutgoing(c ClaimView) LiveStatus { + st := liveOutgoingState(c) + guid := string(c.M2MGUID) + var quik []*QuikLink + var depoL *DepoLink + var watch *OrderWatch + if guid != "" { + quik = globalQuikLinks.ForGUID(guid) + if globalDepoLinks != nil { + depoL = globalDepoLinks.Get(guid) + } + if globalOrderWatch != nil { + watch = globalOrderWatch.Get(guid) + } + } + st.Sig = liveSig(st.Text, c.Status, c.UpdatedAt, sendFailGet(guid), quik, depoL, watch) + return st +} + +// liveIncoming — состояние входящего перевода с отпечатком карточки. +func liveIncoming(it *IncomingTransfer) LiveStatus { + st := liveIncomingState(it) + if it != nil { + var watch *OrderWatch + if globalOrderWatch != nil { + watch = globalOrderWatch.Get(it.GUID) + } + st.Sig = liveSig(st.Text, it, watch) + } + return st } // form16Snapshot — черновики и подачи поручений 16 по всем сделкам одним обходом входящего @@ -49,8 +93,8 @@ func form16Snapshot() map[string]*OutgoingDeal { return m } -// liveOutgoing — состояние исходящего перевода: сделка M2M + депозитарное поручение 16. -func liveOutgoing(c ClaimView) LiveStatus { +// liveOutgoingState — состояние исходящего перевода: сделка M2M + депозитарное поручение 16. +func liveOutgoingState(c ClaimView) LiveStatus { guid := string(c.M2MGUID) var depo *OutgoingDeal if guid != "" && c.Status == "confirmed" { // каталог нужен только на этапе депо-исполнения @@ -110,8 +154,8 @@ func liveOutgoing(c ClaimView) LiveStatus { return LiveStatus{Text: russianState(c.Status), Kind: "wait"} } -// liveIncoming — состояние входящего перевода: наше решение, затем поручение 16/1. -func liveIncoming(it *IncomingTransfer) LiveStatus { +// liveIncomingState — состояние входящего перевода: наше решение, затем поручение 16/1. +func liveIncomingState(it *IncomingTransfer) LiveStatus { if it == nil { return LiveStatus{} } diff --git a/internal/lkgateway/web/templates/admin_claim.html b/internal/lkgateway/web/templates/admin_claim.html index 6919c59..cbd3f83 100644 --- a/internal/lkgateway/web/templates/admin_claim.html +++ b/internal/lkgateway/web/templates/admin_claim.html @@ -35,7 +35,7 @@ Перевод не доставлен в НРД — {{sendFailText .}}.
Попытка {{.At.Format "02.01.2006 15:04:05"}}{{if .PackageID}}, пакет ИШ {{.PackageID}}{{end}}{{if gt .Attempts 1}}, попыток: {{.Attempts}}{{end}}. Отправка в DEPO не отмечена, блокировка в QUIK МНП сохраняется. Когда НРД снова будет доступен, нажмите «Отправить повторно» — уйдёт тот же запрос с тем же GUID. {{end}} - {{$l := liveOut .Claim}}
{{$l.Text}}
+ {{$l := liveOut .Claim}}
{{$l.Text}}
diff --git a/internal/lkgateway/web/templates/admin_incoming_detail.html b/internal/lkgateway/web/templates/admin_incoming_detail.html index c9ace58..d8a85c3 100644 --- a/internal/lkgateway/web/templates/admin_incoming_detail.html +++ b/internal/lkgateway/web/templates/admin_incoming_detail.html @@ -21,7 +21,7 @@

Входящий перевод {{short 8 .It.GUID}} {{if $answered}}отвечено{{else}}ждёт решения{{end}}

- {{$l := liveIn .It}}
{{$l.Text}}
+ {{$l := liveIn .It}}
{{$l.Text}}
Создана{{.Claim.CreatedAt.Format "02.01.2006 15:04:05"}}
Обновлена{{.Claim.UpdatedAt.Format "02.01.2006 15:04:05"}}
diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index eae68fd..738fe3b 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -158,6 +158,12 @@ tbody tr:hover td { background:var(--accent-weak); } .live-bad { color:var(--err) } .live-bad::before { background:var(--err) } .live-big { font-size:14px; margin:-6px 0 12px; font-weight:500 } .live[data-active] { color:var(--text-2) } .live-warn[data-active] { color:var(--warn) } +.live-meta { font-size:12px; color:var(--muted); margin:2px 0 10px; display:flex; align-items:center; gap:6px; } +.live-meta .dot { width:7px; height:7px; border-radius:50%; background:var(--ok,#2e7d32); display:inline-block; animation:livedot 2s ease-in-out infinite; } +.live-meta.changed { color:var(--warn,#8a5a00); } +.live-meta.lost { color:var(--err,#b3261e); } +@keyframes livedot { 50% { opacity:.3 } } +@media (prefers-reduced-motion: reduce) { .live-meta .dot { animation:none } } .badge.draft, .badge.validated, .badge.submitted_to_nsd { background:var(--accent-weak); color:var(--accent); } .badge.awaiting_decision, .badge.manual_approval { background:var(--warn-weak); color:var(--warn); } .badge.confirmed, .badge.awaiting_sub16, .badge.done { background:var(--ok-weak); color:var(--ok); } @@ -550,7 +556,22 @@ label { font-size:13px; font-weight:500; } poll(); setInterval(poll, 12000); // каждые 12 сек // Онлайн-статус заявок в работе: строки с data-live и data-active опрашиваются, пока ждут. + // Карточка заявки (live-big с data-sig): изменилось что-то кроме строки статуса — страница + // перерисовывается сама; если оператор уже начал заполнять форму — только предложение обновить. (function(){ + var dirty = false; + document.addEventListener('input', 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){} + function hhmmss(){ var d = new Date(); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2); } + function meta(e, text, cls){ + var m = e.nextElementSibling; + if (!m || !m.classList.contains('live-meta')) { m = document.createElement('div'); m.className = 'live-meta'; e.parentNode.insertBefore(m, e.nextSibling); } + m.className = 'live-meta' + (cls ? ' ' + cls : ''); m.innerHTML = text; + } + document.querySelectorAll('.live-big[data-live]').forEach(function(e){ + meta(e, e.hasAttribute('data-active') ? 'обновляется автоматически' : 'заявка завершена — автообновление не требуется'); + }); function tick(){ var els = document.querySelectorAll('.live[data-live][data-active]'); if (!els.length) return; @@ -561,11 +582,23 @@ label { font-size:13px; font-weight:500; } els.forEach(function(e){ var k = e.getAttribute('data-live').split(':'), st = (d[k[0]]||{})[k[1]]; if (!st) return; + var big = e.classList.contains('live-big'), was = e.getAttribute('data-sig'); + if (big && was && st.sig && st.sig !== was) { + if (!dirty) { location.reload(); return; } + meta(e, 'Заявка изменилась. Обновить страницу (отмеченное в форме не сохранится)', 'changed'); + e.setAttribute('data-sig', st.sig); + } e.textContent = st.text; - e.className = 'live ' + (e.classList.contains('live-big') ? 'live-big ' : '') + 'live-' + st.kind; + e.className = 'live ' + (big ? 'live-big ' : '') + 'live-' + st.kind; if (!st.active) e.removeAttribute('data-active'); + var nx = e.nextElementSibling; + if (big && !(nx && nx.classList.contains('changed'))) { + meta(e, st.active ? 'обновляется автоматически · проверено в ' + hhmmss() : 'заявка завершена — автообновление не требуется'); + } }); - }).catch(function(){}); + }).catch(function(){ + document.querySelectorAll('.live-big[data-live][data-active]').forEach(function(e){ meta(e, 'нет связи с bj — статус не обновляется, повторим через несколько секунд', 'lost'); }); + }); } setInterval(tick, 8000); if (document.querySelector('.live[data-live][data-active]')) setTimeout(tick, 1500);
Запрос сформирован (отправитель){{msk .It.RequestCreated}}
Забрали из шлюза (получено нами){{msk .It.ReceivedAt}}{{if not .It.RequestCreated.IsZero}} · прохождение: {{transit .It.RequestCreated .It.ReceivedAt}}{{end}}