2aa682e97f
Общий цвет цепочки — по участкам с известным состоянием: несделанная M2M-проверка больше не делает серым работающий обмен. Вместо title — всплывающая панель по участкам с цветными точками, без повтора «последнего ответа НРД». Раздел НРД: наверху плашка, пока анкета участника не принята НРД (не отправлена / ждёт ответа / отклонена с текстом НРД), со ссылкой на анкету; пропадает после приёма. Только в боевом контуре. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
597 lines
20 KiB
Go
597 lines
20 KiB
Go
package lkgateway
|
||
|
||
// Связь с НРД — «зелёный только при свежем подтверждении». Состояние складывается из участков
|
||
// пути, у каждого своё доказательство работы:
|
||
// - Шлюз: ИШ отвечает bj (igwwatch.go);
|
||
// - НРД: шлюз успешно опрашивает НРД — по журналу ИШ (nsdlinkwatch.go);
|
||
// - M2M: сквозная проверка — раз в 10 минут bj запрашивает у НРД справочник участников (#M2MHR)
|
||
// и ждёт ответ #M2MTH: он проходит весь путь до сервиса M2M и обратно;
|
||
// - Отправка: подтверждение доставки каждого пакета (igw.WithDeliveryHook) и недоставленные переводы.
|
||
// Отсутствие ошибок само по себе не считается «работает»: нет свежего подтверждения — жёлтый.
|
||
// Раз в минуту состояние пишется в историю (сутки), по ней на странице «Статус» рисуется полоса.
|
||
// Недоставленные переводы уходят повторно сами, как только НРД снова отвечает (до 24 ч — срок ответа НРД).
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html/template"
|
||
"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 s.State != segUnknown && segRank(s.State) > segRank(h.State) {
|
||
h.State = s.State
|
||
}
|
||
}
|
||
if h.State == segOK && !nsdLinkOK(h) {
|
||
h.State = segUnknown
|
||
}
|
||
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 ""
|
||
}
|
||
|
||
// nsdSegPop — всплывающая панель цепочки в шапке: участки с цветной точкой. Строки «последний ответ»
|
||
// нет — время ответа уже в участке «НРД».
|
||
func nsdSegPop(h NSDHealth) template.HTML {
|
||
esc := template.HTMLEscapeString
|
||
var b strings.Builder
|
||
b.WriteString(`<span class="nsd-pop" role="tooltip"><span class="nsd-pop-b">`)
|
||
for _, s := range h.Segs {
|
||
b.WriteString(`<span class="np-row"><i class="np-d ` + esc(s.State) + `"></i><b>` + esc(s.Title) + `</b><span>` + esc(s.Text))
|
||
if s.Detail != "" {
|
||
b.WriteString(`<small>` + esc(s.Detail) + `</small>`)
|
||
}
|
||
b.WriteString(`</span></span>`)
|
||
}
|
||
b.WriteString(`<span class="np-foot">Подробно — «Статус»</span></span></span>`)
|
||
return template.HTML(b.String())
|
||
}
|
||
|
||
// 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")
|
||
}
|
||
if h.Ago != "" {
|
||
b.WriteString("Последний ответ НРД " + h.Ago + "\n")
|
||
} else {
|
||
b.WriteString("Ответа НРД ещё не было\n")
|
||
}
|
||
b.WriteString("Подробно — «Статус». Обновляется каждые 30 с.")
|
||
return b.String()
|
||
}
|