Мониторинг связи с НРД: участки Шлюз/НРД/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:
zuevav
2026-09-25 16:17:25 +03:00
parent a0f6ce9f7b
commit 4cbefa41d5
18 changed files with 1075 additions and 62 deletions
+31
View File
@@ -55,6 +55,34 @@ type Server struct {
handlerOnce sync.Once
handler http.Handler
nsdMu sync.Mutex // состояние обмена с НРД для баннера (кэш 30 с)
nsdAt time.Time
nsdNotice string
}
// nsdBanner — предупреждение «обмен с НРД недоступен» для вошедшего клиента (пусто — всё работает).
// Спрашивает ядро не чаще раза в 30 с; ядро недоступно — баннер не показываем (об этом скажет сама страница).
func (s *Server) nsdBanner(r *http.Request) string {
sess := session(r)
if sess == "" {
return ""
}
s.nsdMu.Lock()
defer s.nsdMu.Unlock()
if time.Since(s.nsdAt) < 30*time.Second {
return s.nsdNotice
}
var out struct {
Notice string `json:"notice"`
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
if err := s.bj().call(ctx, http.MethodGet, "/api/lk/nsd-status", sess, nil, &out); err == nil {
s.nsdNotice = out.Notice
}
s.nsdAt = time.Now()
return s.nsdNotice
}
// New — кабинет с разобранными шаблонами.
@@ -734,6 +762,9 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, dat
data["AsClient"] = c.Value
}
data["CSRF"] = csrfToken(w, r)
if _, ok := data["NSDNotice"]; !ok {
data["NSDNotice"] = s.nsdBanner(r)
}
if _, ok := data["Flash"]; !ok {
if f := takeFlash(w, r); f != "" {
data["Flash"] = f
+4 -1
View File
@@ -60,6 +60,7 @@ a{color:var(--acc)}a:hover{color:#1e3395}
.stat .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:22px}
.rside{display:flex;flex-direction:column;align-items:flex-end;gap:10px}
.asof{font-size:11.5px;color:#8e8879;letter-spacing:.04em}
.nsd-note{background:var(--err-bg);color:var(--err-tx);padding:10px 20px;font-size:13.5px;display:flex;gap:10px;align-items:center}
.imp{background:var(--board);color:#f3f0e7;padding:10px 20px;font-size:13px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
main{max-width:1180px;margin:0 auto;padding:22px 20px 34px}
h1{font-size:23px;margin:0 0 6px;letter-spacing:-.01em}h2{font-size:15px;margin:0 0 12px;letter-spacing:-.005em}
@@ -137,7 +138,9 @@ input[type=checkbox]{width:20px;min-height:20px;height:20px}
{{if .AsClient}}<div class="imp">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="#e8a33d" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6.5"/><path d="M8 5v4M8 11h.01"/></svg>
<span>Вы в кабинете клиента <b>{{.AsClient}}</b> от имени сотрудника. Поручение можно подготовить, подписывает его клиент.</span>
<form method="post" action="/back-to-admin" style="margin:0"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="lnk" type="submit" style="color:#e8a33d">вернуться к списку клиентов</button></form></div>{{end}}{{end}}
<form method="post" action="/back-to-admin" style="margin:0"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="lnk" type="submit" style="color:#e8a33d">вернуться к списку клиентов</button></form></div>{{end}}{{with .NSDNotice}}<div class="nsd-note" role="status">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6.5"/><path d="M8 5v4M8 11h.01"/></svg>
<span>{{.}}</span></div>{{end}}{{end}}
{{define "login"}}{{template "head" .}}
<div class="login">