From 2aa682e97f2a5fd97a39ebdaff64222c15b6e0c5 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Thu, 22 Oct 2026 10:58:24 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A8=D0=B0=D0=BF=D0=BA=D0=B0=20=C2=AB=D0=A8?= =?UTF-8?q?=D0=BB=D1=8E=D0=B7=20=D0=9D=D0=A0=D0=94=C2=BB:=20=D0=B7=D0=B5?= =?UTF-8?q?=D0=BB=D1=91=D0=BD=D0=B0=D1=8F=20=D0=BF=D1=80=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=8E=D1=89=D0=B5=D0=BC=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BC=D0=B5=D0=BD=D0=B5,=20=D0=B2=D1=81=D0=BF=D0=BB?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=8E=D1=89=D0=B0=D1=8F=20=D0=BF=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C;=20=D0=BF=D0=BB=D0=B0=D1=88=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B0=D0=BD=D0=BA=D0=B5=D1=82=D1=8B=20=D0=B2=20=D0=9D=D0=A0?= =?UTF-8?q?=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Общий цвет цепочки — по участкам с известным состоянием: несделанная M2M-проверка больше не делает серым работающий обмен. Вместо title — всплывающая панель по участкам с цветными точками, без повтора «последнего ответа НРД». Раздел НРД: наверху плашка, пока анкета участника не принята НРД (не отправлена / ждёт ответа / отклонена с текстом НРД), со ссылкой на анкету; пропадает после приёма. Только в боевом контуре. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn --- internal/lkgateway/admin.go | 26 ++++++++++------- internal/lkgateway/nsdhealth.go | 25 +++++++++++++++- internal/lkgateway/participant_reg.go | 29 +++++++++++++++++++ .../lkgateway/web/templates/admin_setup.html | 4 ++- internal/lkgateway/web/templates/layout.html | 24 +++++++++++---- 5 files changed, 89 insertions(+), 19 deletions(-) diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 34e2469..d3603fe 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -153,17 +153,21 @@ var templateFuncs = template.FuncMap{ "looksErr": looksErr, "intList": func(v ...int) []int { return v }, "nsdSegTip": nsdSegTip, - "devOn": authTemplateFuncs["devOn"], - "devLicensed": authTemplateFuncs["devLicensed"], - "mod": func(f string) bool { return globalRC == nil || moduleAllowed(globalRC, f) }, // модуль в лицензии - "modTitle": moduleTitle, // название признака лицензии по-человечески - "isIIS": incomingIsIIS, // входящий перевод на ИИС - "depoSubs": func() any { return depoWizardSubs }, // экраны шага 3 мастера - "authOpen": authOpen, - "internalDocs": func() bool { return internalDocsBuilt }, - "roleTitle": roleTitle, - "stageCls": func(r depoQueueRow) string { c, _ := r.StageRu(); return c }, - "stageText": func(r depoQueueRow) string { _, t := r.StageRu(); return t }, + "nsdSegPop": nsdSegPop, + // participantOK / participantBanner — анкета участника M2M принята НРД; плашка наверху раздела НРД, пока нет. + "participantOK": participantAccepted, + "participantBanner": participantBanner, + "devOn": authTemplateFuncs["devOn"], + "devLicensed": authTemplateFuncs["devLicensed"], + "mod": func(f string) bool { return globalRC == nil || moduleAllowed(globalRC, f) }, // модуль в лицензии + "modTitle": moduleTitle, // название признака лицензии по-человечески + "isIIS": incomingIsIIS, // входящий перевод на ИИС + "depoSubs": func() any { return depoWizardSubs }, // экраны шага 3 мастера + "authOpen": authOpen, + "internalDocs": func() bool { return internalDocsBuilt }, + "roleTitle": roleTitle, + "stageCls": func(r depoQueueRow) string { c, _ := r.StageRu(); return c }, + "stageText": func(r depoQueueRow) string { _, t := r.StageRu(); return t }, "deref": func(v *float64) float64 { if v == nil { return 0 diff --git a/internal/lkgateway/nsdhealth.go b/internal/lkgateway/nsdhealth.go index 73d169b..772dbc0 100644 --- a/internal/lkgateway/nsdhealth.go +++ b/internal/lkgateway/nsdhealth.go @@ -15,6 +15,7 @@ import ( "context" "encoding/json" "fmt" + "html/template" "log" "net/http" "os" @@ -234,12 +235,17 @@ func nsdHealthNow(rc *RuntimeConfig, now time.Time) NSDHealth { nsdH.Unlock() h.Segs = []NSDSeg{gw, ns, m, sd} + // Общий цвет — по участкам с известным состоянием: «проверка ещё не выполнялась» не делает + // серым работающий обмен. Серый — только когда по известным участкам связь не подтверждена. h.State = segOK for _, s := range h.Segs { - if segRank(s.State) > segRank(h.State) { + 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 } @@ -553,6 +559,23 @@ func nsdClientNotice(h NSDHealth) string { return "" } +// nsdSegPop — всплывающая панель цепочки в шапке: участки с цветной точкой. Строки «последний ответ» +// нет — время ответа уже в участке «НРД». +func nsdSegPop(h NSDHealth) template.HTML { + esc := template.HTMLEscapeString + var b strings.Builder + b.WriteString(``) + for _, s := range h.Segs { + b.WriteString(`` + esc(s.Title) + `` + esc(s.Text)) + if s.Detail != "" { + b.WriteString(`` + esc(s.Detail) + ``) + } + b.WriteString(``) + } + b.WriteString(`Подробно — «Статус»`) + return template.HTML(b.String()) +} + // nsdSegTip — подсказка к цепочке в шапке: все участки построчно. func nsdSegTip(h NSDHealth) string { var b strings.Builder diff --git a/internal/lkgateway/participant_reg.go b/internal/lkgateway/participant_reg.go index 4ee377e..55f17c8 100644 --- a/internal/lkgateway/participant_reg.go +++ b/internal/lkgateway/participant_reg.go @@ -6,6 +6,7 @@ package lkgateway import ( "encoding/json" + "html/template" "os" "strings" "sync" @@ -138,3 +139,31 @@ func recordParticipantReply(status string, resp *m2m.M2MTransferResponse, rawFil r.RawFile = rawFile }) } + +// participantAccepted — последняя отправленная анкета принята НРД. +func participantAccepted() bool { + r := loadParticipantReg() + return !r.RespAt.IsZero() && !strings.EqualFold(r.Status, "ERROR") +} + +// participantBanner — плашка наверху раздела НРД: анкета не отправлена, ждёт ответа или отклонена. +// Пропадает, когда НРД принял анкету. Анкету шлём только в боевом контуре — в тестовом плашки нет. +func participantBanner() template.HTML { + if participantAccepted() || contourOf(currentNSDProfile()) != "prod" { + return "" + } + esc := template.HTMLEscapeString + r := loadParticipantReg() + cls, head, body := "stc-err", "Анкета участника M2M не отправлена", + "Без регистрации участника НРД не принимает запросы на переводы. Проверьте анкету и отправьте её." + switch { + case r.SentAt.IsZero(): + case r.RespAt.IsZero(): + cls, head, body = "stc-run", "Анкета участника M2M ждёт ответа НРД", "Отправлена "+r.SentAt.In(listMSK).Format("02.01.2006 15:04")+". Плашка пропадёт, когда НРД примет анкету." + case strings.EqualFold(r.Status, "ERROR"): + cls, head = "stc-err", "НРД отклонил анкету участника M2M" + body = r.RespAt.In(listMSK).Format("02.01.2006 15:04") + ": " + r.RespText + ". Исправьте и отправьте снова." + } + return template.HTML(`
` + esc(head) + + `` + esc(body) + ` Открыть анкету →
`) +} diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 3efce70..3fbd52c 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -335,6 +335,7 @@ {{/* ============ НРД ============ */}}

НРД

+ {{participantBanner}} {{template "nsdStatus" .}}

Шлюз НРД

@@ -342,7 +343,8 @@ {{template "igateInstall" .}}
-

Регистрация участника M2M

+

Регистрация участника M2M

+ {{if participantOK}}

✓ Анкета принята НРД — организация зарегистрирована в сервисе M2M.

{{end}}

НРД требует анкету участника перед началом работы: без регистрации НРД не принимает запросы на переводы. bj заполняет анкету по реквизитам организации, даёт проверить и отправляет в НРД через шлюз (или её можно отправить письмом на M2MOST@nsd.ru).

Открыть анкету участника diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index 6081049..c9108cd 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -112,6 +112,16 @@ body { .nsd-chain.ok i { background:var(--ok); } .nsd-chain.warn i { background:var(--warn); } .nsd-chain.err i { background:var(--err); } .nsd-chain.err { background:var(--err-weak); border-color:var(--err); color:var(--err); } .nsd-chain.warn { background:var(--warn-weak); } +.nsd-chain { position:relative; } +.nsd-pop { display:none; position:absolute; top:100%; right:0; padding-top:8px; z-index:60; cursor:default; } +.nsd-chain:hover .nsd-pop, .nsd-chain:focus .nsd-pop { display:block; } +.nsd-pop-b { display:block; min-width:300px; max-width:420px; background:var(--card); color:var(--text); border:1px solid var(--border); border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,.14); padding:10px 14px; font-size:13px; line-height:1.4; text-align:left; white-space:normal; font-weight:400; } +.np-row { display:grid; grid-template-columns:10px 72px 1fr; gap:8px; align-items:baseline; padding:4px 0; } +.np-row + .np-row { border-top:1px solid var(--border); } +.np-row small { display:block; color:var(--muted); font-size:11.5px; } +.np-d { width:8px; height:8px; border-radius:50%; background:var(--muted); display:inline-block; } +.np-d.ok { background:var(--ok); } .np-d.warn { background:var(--warn); } .np-d.err { background:var(--err); } +.np-foot { display:block; margin-top:6px; color:var(--muted); font-size:12px; } .sys-block { display:inline-flex; flex-direction:column; align-items:center; gap:2px; line-height:1.1; } .sys-block .sys-status { padding:3px 9px 3px 7px; font-size:11.5px; } .user-dd .ddbtn.user-chip { display:inline-flex; flex-direction:column; align-items:flex-start; padding:4px 10px; border-radius:8px; margin:0; max-width:220px; } @@ -587,7 +597,7 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd
- {{if and .NSDLink.Configured (not .IsMockMode)}}Шлюз НРД{{else}} + {{if and .NSDLink.Configured (not .IsMockMode)}}Шлюз НРД{{nsdSegPop .NSDLink}}{{else}} {{if .IsMockMode}}{{if devOn}}Эмуляция{{else}}НРД не подключён{{end}}{{else}}НРД на связи{{end}}{{end}} {{.Now}} @@ -837,14 +847,16 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd function upd(){ fetch('/admin/nsd-health.json', {cache:'no-store'}).then(function(r){ return r.json(); }).then(function(h){ if (!h.configured) return; - var tip = ''; - (h.segs||[]).forEach(function(s){ tip += s.title + ': ' + s.text + (s.detail ? ' (' + s.detail + ')' : '') + '\n'; }); - tip += (h.ago ? 'Последний ответ НРД ' + h.ago : 'Ответа НРД ещё не было') + '\n'; + var esc = function(t){ var d = document.createElement('span'); d.textContent = t || ''; return d.innerHTML; }; + var pop = ''; + (h.segs||[]).forEach(function(s){ pop += '' + esc(s.title) + '' + esc(s.text) + (s.detail ? '' + esc(s.detail) + '' : '') + ''; }); el.className = 'sys-status nsd-chain ' + h.state; - el.title = tip + 'Подробно — «Статус». Обновляется каждые 30 с.'; + var b = el.querySelector('.nsd-pop-b'); + if (b) b.innerHTML = pop + 'Подробно — «Статус»'; }).catch(function(){ el.className = 'sys-status nsd-chain warn'; - el.title = 'Нет связи с bj — состояние НРД не обновляется'; + var b = el.querySelector('.nsd-pop-b'); + if (b) b.innerHTML = 'bjнет связи — состояние НРД не обновляется'; }); } setInterval(upd, 30000);