Мониторинг связи с НРД: участки Шлюз/НРД/M2M/Отправка, сквозная проверка справочником раз в 10 мин, история за сутки на «Статусе», цепочка в шапке (30 с), автоповтор недоставленных до 24 ч, баннер и статус «ожидает отправки» в кабинете клиента
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
package lkgateway
|
||||
|
||||
// Связь с НРД — «зелёный только при свежем подтверждении». Состояние складывается из участков
|
||||
// пути, у каждого своё доказательство работы:
|
||||
// - Шлюз: ИШ отвечает bj (igwwatch.go);
|
||||
// - НРД: шлюз успешно опрашивает НРД — по журналу ИШ (nsdlinkwatch.go);
|
||||
// - M2M: сквозная проверка — раз в 10 минут bj запрашивает у НРД справочник участников (#M2MHR)
|
||||
// и ждёт ответ #M2MTH: он проходит весь путь до сервиса M2M и обратно;
|
||||
// - Отправка: подтверждение доставки каждого пакета (igw.WithDeliveryHook) и недоставленные переводы.
|
||||
// Отсутствие ошибок само по себе не считается «работает»: нет свежего подтверждения — жёлтый.
|
||||
// Раз в минуту состояние пишется в историю (сутки), по ней на странице «Статус» рисуется полоса.
|
||||
// Недоставленные переводы уходят повторно сами, как только НРД снова отвечает (до 24 ч — срок ответа НРД).
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
|
||||
)
|
||||
|
||||
const (
|
||||
nsdProbeEvery = 10 * time.Minute
|
||||
nsdProbeWarnAfter = 3 * time.Minute // ответа на проверку нет 3 мин — жёлтый
|
||||
nsdProbeErrAfter = 10 * time.Minute // 10 мин — красный
|
||||
nsdPollErrAfter = 3 * time.Minute // шлюз получает от НРД ошибки 3 мин — красный (до этого жёлтый)
|
||||
nsdPollStale = 5 * time.Minute // нет свежих опросов НРД в журнале — жёлтый
|
||||
autoResendMaxAge = 24 * time.Hour // срок ответа НРД: старше — не повторяем сами
|
||||
autoResendGap = 5 * time.Minute // между попытками по одной заявке
|
||||
nsdHistoryKeep = 24 * 60 // минут
|
||||
)
|
||||
|
||||
var nsdHistoryFile = "/var/lib/bj/.bj/nsd-health.json"
|
||||
|
||||
// Состояния участков по возрастанию тяжести.
|
||||
const (
|
||||
segOK = "ok"
|
||||
segUnknown = "unknown"
|
||||
segWarn = "warn"
|
||||
segErr = "err"
|
||||
segOff = "off"
|
||||
)
|
||||
|
||||
func segRank(s string) int {
|
||||
switch s {
|
||||
case segOK:
|
||||
return 0
|
||||
case segUnknown:
|
||||
return 1
|
||||
case segWarn:
|
||||
return 2
|
||||
case segErr:
|
||||
return 3
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// NSDSeg — участок пути до НРД.
|
||||
type NSDSeg struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
Text string `json:"text"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// NSDHealth — сводка для шапки, «Статуса» и кабинета клиента.
|
||||
type NSDHealth struct {
|
||||
Configured bool `json:"configured"`
|
||||
State string `json:"state"` // худшее из участков
|
||||
Segs []NSDSeg `json:"segs"`
|
||||
LastAnswer time.Time `json:"last_answer"` // последнее подтверждение от НРД (любое)
|
||||
Ago string `json:"ago"` // «40 с назад»
|
||||
Failed int `json:"failed"` // недоставленных переводов
|
||||
}
|
||||
|
||||
var nsdH struct {
|
||||
sync.Mutex
|
||||
ProbeSentAt time.Time
|
||||
ProbeReplyAt time.Time
|
||||
ProbeLatency time.Duration
|
||||
ProbeErr string
|
||||
LastDelivered time.Time
|
||||
LastDelErrAt time.Time
|
||||
LastDelErr string
|
||||
probeAlerted bool
|
||||
resendNotified map[string]bool
|
||||
history []nsdSample
|
||||
historyLoaded bool
|
||||
historyDirtyCnt int
|
||||
}
|
||||
|
||||
type nsdSample struct {
|
||||
T int64 `json:"t"` // минута (unix/60)
|
||||
S string `json:"s"` // по символу на участок: o w e u -
|
||||
}
|
||||
|
||||
// nsdProbeReply — пришёл справочник: НРД ответил (по нашей проверке или сам).
|
||||
func nsdProbeReply(at time.Time) {
|
||||
nsdH.Lock()
|
||||
defer nsdH.Unlock()
|
||||
if !nsdH.ProbeSentAt.IsZero() && nsdH.ProbeReplyAt.Before(nsdH.ProbeSentAt) && at.After(nsdH.ProbeSentAt) {
|
||||
nsdH.ProbeLatency = at.Sub(nsdH.ProbeSentAt)
|
||||
}
|
||||
nsdH.ProbeReplyAt = at
|
||||
}
|
||||
|
||||
// nsdDelivery — итог передачи пакета в НРД (из клиента ИШ).
|
||||
func nsdDelivery(_ string, status, code string) {
|
||||
nsdH.Lock()
|
||||
defer nsdH.Unlock()
|
||||
switch status {
|
||||
case igw.StatusSent:
|
||||
nsdH.LastDelivered = time.Now()
|
||||
case igw.StatusError:
|
||||
nsdH.LastDelErrAt, nsdH.LastDelErr = time.Now(), code
|
||||
}
|
||||
}
|
||||
|
||||
func agoText(t time.Time, now time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
d := now.Sub(t)
|
||||
switch {
|
||||
case d < time.Minute:
|
||||
return fmt.Sprintf("%d с назад", int(d.Seconds()))
|
||||
case d < time.Hour:
|
||||
return fmt.Sprintf("%d мин назад", int(d.Minutes()))
|
||||
}
|
||||
return humanDur(d) + " назад"
|
||||
}
|
||||
|
||||
// nsdHealthNow — текущее состояние связи с НРД.
|
||||
func nsdHealthNow(rc *RuntimeConfig, now time.Time) NSDHealth {
|
||||
h := NSDHealth{State: segOff}
|
||||
if rc == nil {
|
||||
return h
|
||||
}
|
||||
base := strings.TrimSpace(rc.Snapshot().NSD.IGWBaseURL)
|
||||
if base == "" {
|
||||
return h
|
||||
}
|
||||
h.Configured = true
|
||||
|
||||
// Шлюз
|
||||
igwWatch.mu.Lock()
|
||||
gw := NSDSeg{Key: "igw", Title: "Шлюз"}
|
||||
igwUp := igwWatch.Checked && igwWatch.Up
|
||||
switch {
|
||||
case !igwWatch.Checked:
|
||||
gw.State, gw.Text = segUnknown, "проверяется"
|
||||
case igwWatch.Up:
|
||||
gw.State, gw.Text = segOK, "отвечает"
|
||||
default:
|
||||
gw.State, gw.Text, gw.Detail = segErr, "не отвечает "+humanDur(now.Sub(igwWatch.DownSince)), igwWatch.LastErr
|
||||
}
|
||||
igwWatch.mu.Unlock()
|
||||
|
||||
// НРД (опросы шлюза)
|
||||
nsdWatch.Lock()
|
||||
ns := NSDSeg{Key: "nsd", Title: "НРД"}
|
||||
lastPollOK := nsdWatch.LastPollOK
|
||||
switch {
|
||||
case !igwLocal(base):
|
||||
ns.State, ns.Text = segUnknown, "журнал шлюза недоступен (шлюз на другом сервере)"
|
||||
case !igwUp:
|
||||
ns.State, ns.Text = segUnknown, "неизвестно — шлюз не отвечает"
|
||||
case nsdWatch.LastPollAt.IsZero():
|
||||
ns.State, ns.Text = segUnknown, "нет данных опроса"
|
||||
case !nsdWatch.DownSince.IsZero():
|
||||
down := now.Sub(nsdWatch.DownSince)
|
||||
ns.State = segWarn
|
||||
if down >= nsdPollErrAfter {
|
||||
ns.State = segErr
|
||||
}
|
||||
ns.Text = "отвечает шлюзу ошибками " + humanDur(down)
|
||||
ns.Detail = nsdWatch.Contour + ": " + nsdWatch.LastErr
|
||||
case now.Sub(nsdWatch.LastPollOK) > nsdPollStale:
|
||||
ns.State, ns.Text = segWarn, "давно нет опросов НРД (последний "+agoText(nsdWatch.LastPollOK, now)+")"
|
||||
default:
|
||||
ns.State, ns.Text = segOK, "отвечает · "+agoText(nsdWatch.LastPollOK, now)
|
||||
}
|
||||
nsdWatch.Unlock()
|
||||
|
||||
nsdH.Lock()
|
||||
// M2M — сквозная проверка
|
||||
m := NSDSeg{Key: "m2m", Title: "M2M"}
|
||||
pending := !nsdH.ProbeSentAt.IsZero() && nsdH.ProbeReplyAt.Before(nsdH.ProbeSentAt)
|
||||
switch {
|
||||
case nsdH.ProbeSentAt.IsZero() && nsdH.ProbeReplyAt.IsZero():
|
||||
m.State, m.Text = segUnknown, "проверка ещё не выполнялась"
|
||||
case nsdH.ProbeErr != "":
|
||||
m.State, m.Text, m.Detail = segErr, "запрос проверки не доставлен", nsdH.ProbeErr
|
||||
case pending && now.Sub(nsdH.ProbeSentAt) >= nsdProbeErrAfter:
|
||||
m.State, m.Text = segErr, "НРД не ответил на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt))
|
||||
case pending && now.Sub(nsdH.ProbeSentAt) >= nsdProbeWarnAfter:
|
||||
m.State, m.Text = segWarn, "ждём ответ НРД на проверку "+humanDur(now.Sub(nsdH.ProbeSentAt))
|
||||
case pending && nsdH.ProbeReplyAt.IsZero():
|
||||
m.State, m.Text = segUnknown, "проверка отправлена, ждём ответ"
|
||||
default:
|
||||
m.State, m.Text = segOK, "ответ на проверку "+agoText(nsdH.ProbeReplyAt, now)
|
||||
if nsdH.ProbeLatency > 0 {
|
||||
m.Text += fmt.Sprintf(" (за %d с)", int(nsdH.ProbeLatency.Seconds()))
|
||||
}
|
||||
if pending {
|
||||
m.Text += " · идёт новая проверка"
|
||||
}
|
||||
}
|
||||
// Отправка
|
||||
sd := NSDSeg{Key: "send", Title: "Отправка"}
|
||||
h.Failed = sendFailCount()
|
||||
switch {
|
||||
case h.Failed > 0:
|
||||
sd.State, sd.Text = segErr, fmt.Sprintf("не доставлено переводов: %d — отправятся повторно сами, когда НРД ответит", h.Failed)
|
||||
case !nsdH.LastDelErrAt.IsZero() && nsdH.LastDelErrAt.After(nsdH.LastDelivered):
|
||||
sd.State, sd.Text, sd.Detail = segWarn, "последний пакет не доставлен "+agoText(nsdH.LastDelErrAt, now), nsdH.LastDelErr
|
||||
case !nsdH.LastDelivered.IsZero():
|
||||
sd.State, sd.Text = segOK, "последний пакет доставлен "+agoText(nsdH.LastDelivered, now)
|
||||
default:
|
||||
sd.State, sd.Text = segOK, "ошибок доставки нет"
|
||||
}
|
||||
for _, t := range []time.Time{lastPollOK, nsdH.ProbeReplyAt, nsdH.LastDelivered} {
|
||||
if t.After(h.LastAnswer) {
|
||||
h.LastAnswer = t
|
||||
}
|
||||
}
|
||||
nsdH.Unlock()
|
||||
|
||||
h.Segs = []NSDSeg{gw, ns, m, sd}
|
||||
h.State = segOK
|
||||
for _, s := range h.Segs {
|
||||
if segRank(s.State) > segRank(h.State) {
|
||||
h.State = s.State
|
||||
}
|
||||
}
|
||||
h.Ago = agoText(h.LastAnswer, now)
|
||||
return h
|
||||
}
|
||||
|
||||
// nsdLinkOK — НРД доступен для отправки: шлюз отвечает и (по журналу или проверке) НРД отвечает.
|
||||
func nsdLinkOK(h NSDHealth) bool {
|
||||
st := map[string]string{}
|
||||
for _, s := range h.Segs {
|
||||
st[s.Key] = s.State
|
||||
}
|
||||
if st["igw"] != segOK {
|
||||
return false
|
||||
}
|
||||
switch st["nsd"] {
|
||||
case segOK:
|
||||
return true
|
||||
case segUnknown: // журнала нет — судим по сквозной проверке
|
||||
return st["m2m"] == segOK
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- проверка связи ---
|
||||
|
||||
var nsdProbeNowCh = make(chan struct{}, 1)
|
||||
|
||||
// nsdProbeNow — «Проверить связь сейчас».
|
||||
func nsdProbeNow() {
|
||||
select {
|
||||
case nsdProbeNowCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) sendNSDProbe(ctx context.Context) {
|
||||
nsdH.Lock()
|
||||
nsdH.ProbeSentAt, nsdH.ProbeErr = time.Now(), ""
|
||||
nsdH.Unlock()
|
||||
cctx, cancel := context.WithTimeout(ctx, 40*time.Second)
|
||||
defer cancel()
|
||||
if _, err := sendHandbookRequest(cctx, s.igwClient, s.igwChannel); err != nil {
|
||||
nsdH.Lock()
|
||||
nsdH.ProbeErr = err.Error()
|
||||
nsdH.Unlock()
|
||||
log.Printf("nsd-health: проверка связи (#M2MHR) не отправлена: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// nsdHealthLoop — проверка раз в 10 мин, история раз в минуту, автоповтор недоставленных.
|
||||
func (s *Server) nsdHealthLoop(ctx context.Context) {
|
||||
healthRegister("nsd-health", 3*time.Minute)
|
||||
nsdHistoryLoad()
|
||||
probeT := time.NewTimer(time.Minute)
|
||||
defer probeT.Stop()
|
||||
minute := time.NewTicker(time.Minute)
|
||||
defer minute.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
nsdHistorySave()
|
||||
return
|
||||
case <-nsdProbeNowCh:
|
||||
if s.igwClient != nil {
|
||||
s.sendNSDProbe(ctx)
|
||||
}
|
||||
case <-probeT.C:
|
||||
if s.igwClient != nil {
|
||||
s.sendNSDProbe(ctx)
|
||||
}
|
||||
probeT.Reset(nsdProbeEvery)
|
||||
case <-minute.C:
|
||||
h := nsdHealthNow(s.rc, time.Now())
|
||||
nsdHistoryAdd(h, time.Now())
|
||||
s.nsdProbeAlerts(h)
|
||||
if nsdLinkOK(h) {
|
||||
s.autoResend(ctx)
|
||||
}
|
||||
healthBeat("nsd-health")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nsdProbeAlerts — оповещения по сквозной проверке (сбой опросов НРД сообщает nsdlinkwatch.go).
|
||||
func (s *Server) nsdProbeAlerts(h NSDHealth) {
|
||||
var m NSDSeg
|
||||
for _, x := range h.Segs {
|
||||
if x.Key == "m2m" {
|
||||
m = x
|
||||
}
|
||||
}
|
||||
nsdH.Lock()
|
||||
was := nsdH.probeAlerted
|
||||
switch {
|
||||
case m.State == segErr && !was:
|
||||
nsdH.probeAlerted = true
|
||||
case m.State == segOK && was:
|
||||
nsdH.probeAlerted = false
|
||||
default:
|
||||
nsdH.Unlock()
|
||||
return
|
||||
}
|
||||
nsdH.Unlock()
|
||||
if m.State == segErr {
|
||||
raiseSystemAlert(s.rc, "НРД не отвечает на проверку M2M", "Сквозная проверка связи (запрос справочника участников M2M) не прошла: "+m.Text+". "+m.Detail+
|
||||
" Пока так, переводы и решения могут не доходить до НРД. Недоставленные переводы bj отправит повторно сам, когда связь восстановится.")
|
||||
return
|
||||
}
|
||||
raiseSystemAlert(s.rc, "Проверка связи с НРД снова проходит", "НРД ответил на сквозную проверку M2M: "+m.Text+".")
|
||||
}
|
||||
|
||||
// autoResend — повторная отправка недоставленных переводов, когда НРД снова отвечает.
|
||||
func (s *Server) autoResend(ctx context.Context) {
|
||||
if s.svc == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
for guid, f := range sendFailList() {
|
||||
first := f.FirstAt
|
||||
if first.IsZero() {
|
||||
first = f.At
|
||||
}
|
||||
if now.Sub(first) > autoResendMaxAge {
|
||||
if sendFailGiveUp(guid) {
|
||||
raiseSystemAlert(s.rc, "Перевод не доставлен в НРД за 24 часа",
|
||||
fmt.Sprintf("Перевод %s не удалось доставить в НРД за сутки (попыток: %d). Автоповтор остановлен — срок ответа НРД истёк. Решите в карточке заявки: «Отправить повторно» или «Удалить».", shortGUID(guid), f.Attempts))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if f.GaveUp || now.Sub(f.At) < autoResendGap {
|
||||
continue
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
err := s.svc.ResendClaim(cctx, f.DealID)
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Printf("nsd-health: автоповтор перевода %s: %v", shortGUID(guid), err)
|
||||
continue
|
||||
}
|
||||
log.Printf("nsd-health: перевод %s отправлен повторно автоматически (попытка %d)", shortGUID(guid), f.Attempts+1)
|
||||
raiseSystemAlert(s.rc, "Перевод отправлен в НРД повторно",
|
||||
fmt.Sprintf("Связь с НРД восстановилась — перевод %s отправлен автоматически, НРД принял пакет.", shortGUID(guid)))
|
||||
}
|
||||
}
|
||||
|
||||
// --- история ---
|
||||
|
||||
func nsdSegChar(st string) byte {
|
||||
switch st {
|
||||
case segOK:
|
||||
return 'o'
|
||||
case segWarn:
|
||||
return 'w'
|
||||
case segErr:
|
||||
return 'e'
|
||||
case segUnknown:
|
||||
return 'u'
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
func nsdHistoryLoad() {
|
||||
nsdH.Lock()
|
||||
defer nsdH.Unlock()
|
||||
if nsdH.historyLoaded {
|
||||
return
|
||||
}
|
||||
nsdH.historyLoaded = true
|
||||
if raw, err := os.ReadFile(nsdHistoryFile); err == nil {
|
||||
_ = json.Unmarshal(raw, &nsdH.history)
|
||||
}
|
||||
}
|
||||
|
||||
func nsdHistorySave() {
|
||||
nsdH.Lock()
|
||||
raw, err := json.Marshal(nsdH.history)
|
||||
nsdH.Unlock()
|
||||
if err == nil {
|
||||
_ = os.WriteFile(nsdHistoryFile, raw, 0o600)
|
||||
}
|
||||
}
|
||||
|
||||
func nsdHistoryAdd(h NSDHealth, now time.Time) {
|
||||
if !h.Configured {
|
||||
return
|
||||
}
|
||||
b := make([]byte, len(h.Segs))
|
||||
for i, s := range h.Segs {
|
||||
b[i] = nsdSegChar(s.State)
|
||||
}
|
||||
nsdH.Lock()
|
||||
t := now.Unix() / 60
|
||||
if n := len(nsdH.history); n > 0 && nsdH.history[n-1].T == t {
|
||||
nsdH.history[n-1].S = string(b)
|
||||
} else {
|
||||
nsdH.history = append(nsdH.history, nsdSample{T: t, S: string(b)})
|
||||
}
|
||||
cut := t - nsdHistoryKeep
|
||||
i := 0
|
||||
for i < len(nsdH.history) && nsdH.history[i].T <= cut {
|
||||
i++
|
||||
}
|
||||
nsdH.history = nsdH.history[i:]
|
||||
nsdH.historyDirtyCnt++
|
||||
save := nsdH.historyDirtyCnt >= 5
|
||||
if save {
|
||||
nsdH.historyDirtyCnt = 0
|
||||
}
|
||||
nsdH.Unlock()
|
||||
if save {
|
||||
nsdHistorySave()
|
||||
}
|
||||
}
|
||||
|
||||
// NSDStripCell — отрезок полосы (10 минут).
|
||||
type NSDStripCell struct {
|
||||
State string
|
||||
Title string
|
||||
}
|
||||
|
||||
// NSDStripRow — полоса одного участка за сутки.
|
||||
type NSDStripRow struct {
|
||||
Title string
|
||||
Cells []NSDStripCell
|
||||
Bad string // итог: «сбоев не было» / «сбой 2 ч 10 мин»
|
||||
}
|
||||
|
||||
// nsdStrip — полосы за сутки по 10 минут (худшее состояние в отрезке).
|
||||
func nsdStrip(titles []string, now time.Time) []NSDStripRow {
|
||||
nsdHistoryLoad()
|
||||
nsdH.Lock()
|
||||
hist := append([]nsdSample(nil), nsdH.history...)
|
||||
nsdH.Unlock()
|
||||
const bucket = 10
|
||||
end := now.Unix() / 60
|
||||
start := end - nsdHistoryKeep + 1
|
||||
nb := nsdHistoryKeep / bucket
|
||||
rows := make([]NSDStripRow, len(titles))
|
||||
for i, t := range titles {
|
||||
rows[i] = NSDStripRow{Title: t, Cells: make([]NSDStripCell, nb)}
|
||||
}
|
||||
bad := make([]int, len(titles))
|
||||
for _, smp := range hist {
|
||||
if smp.T < start || smp.T > end {
|
||||
continue
|
||||
}
|
||||
k := int(smp.T-start) / bucket
|
||||
for i := range titles {
|
||||
if i >= len(smp.S) {
|
||||
continue
|
||||
}
|
||||
st := map[byte]string{'o': segOK, 'w': segWarn, 'e': segErr, 'u': segUnknown}[smp.S[i]]
|
||||
if st == "" {
|
||||
continue
|
||||
}
|
||||
if st == segErr {
|
||||
bad[i]++
|
||||
}
|
||||
if segRank(st) > segRank(rows[i].Cells[k].State) || rows[i].Cells[k].State == "" {
|
||||
rows[i].Cells[k].State = st
|
||||
}
|
||||
}
|
||||
}
|
||||
names := map[string]string{segOK: "работает", segWarn: "с перебоями", segErr: "сбой", segUnknown: "нет данных", "": "нет данных"}
|
||||
for i := range rows {
|
||||
for k := range rows[i].Cells {
|
||||
from := time.Unix((start+int64(k*bucket))*60, 0)
|
||||
c := &rows[i].Cells[k]
|
||||
if c.State == "" {
|
||||
c.State = "none"
|
||||
}
|
||||
c.Title = from.Format("15:04") + "–" + from.Add(bucket*time.Minute).Format("15:04") + ": " + names[strings.TrimPrefix(c.State, "none")]
|
||||
}
|
||||
if bad[i] == 0 {
|
||||
rows[i].Bad = "сбоев не было"
|
||||
} else {
|
||||
rows[i].Bad = "сбой " + humanDur(time.Duration(bad[i])*time.Minute)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// --- HTTP ---
|
||||
|
||||
// registerNSDHealth — GET /admin/nsd-health.json (шапка обновляется сама), POST /admin/status/nsd-probe.
|
||||
func registerNSDHealth(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
mux.HandleFunc("/admin/nsd-health.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(nsdHealthNow(rc, time.Now()))
|
||||
})
|
||||
mux.HandleFunc("/admin/status/nsd-probe", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
nsdProbeNow()
|
||||
http.Redirect(w, r, "/admin/status?probe=1#nsd", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
// nsdClientNotice — текст для кабинета клиента (пусто — обмен работает).
|
||||
func nsdClientNotice(h NSDHealth) string {
|
||||
if !h.Configured || nsdLinkOK(h) {
|
||||
return ""
|
||||
}
|
||||
for _, s := range h.Segs {
|
||||
if (s.Key == "igw" || s.Key == "nsd" || s.Key == "m2m") && s.State == segErr {
|
||||
return "Обмен с НРД временно недоступен. Поручения на перевод принимаются и будут отправлены в НРД автоматически, как только связь восстановится."
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// nsdSegTip — подсказка к цепочке в шапке: все участки построчно.
|
||||
func nsdSegTip(h NSDHealth) string {
|
||||
var b strings.Builder
|
||||
for _, s := range h.Segs {
|
||||
b.WriteString(s.Title + ": " + s.Text)
|
||||
if s.Detail != "" {
|
||||
b.WriteString(" (" + s.Detail + ")")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("Подробно — «Статус». Обновляется каждые 30 с.")
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user