ИШ ждёт флешку с ключом — bj это видит и перезапускает шлюз сам; итог установки ключа; список копий; скрытые блоки

- ИШ без флешки ждёт её в окне Валидаты («Вставьте ключевой носитель»): статус «ждёт флешку», без кнопки
  переустановки; сторож не тратит перезапуски, а когда флешка появилась — перезапускает ИШ сам; письмо
- шаг ключа: итог — карточка «Ключ подключён», «Заменить ключ» вместо «Установить ещё один», этапы свёрнуты
- копии: «Копии на хранилище» ищет там же, куда ложится копия (подпапка по умолчанию bj-backup, не из старых
  настроек); поля показываются разобранными
- [hidden] прячет всегда (пустая полоска под версией ИШ); «Шлюз НРД на другом сервере?» — что это за адрес

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-10-07 00:11:48 +03:00
parent 5abf71e3ed
commit c62225dd7d
10 changed files with 86 additions and 21 deletions
+1
View File
@@ -60,6 +60,7 @@ var templateFuncs = template.FuncMap{
"backupStatus": backupStatus,
"backupWhen": backupWhen,
"smbWin": smbWin,
"smbView": smbView,
"pgLocal": pgDSNLocal,
"whForm": func(w WorkHoursSettings, ret string) map[string]any { return map[string]any{"W": w, "Ret": ret} },
"weekdayNames": func() []string { return weekdayShort },
+13 -3
View File
@@ -86,6 +86,16 @@ func smbWin(share string) string {
return strings.ReplaceAll(share, "/", `\`)
}
// smbView — адрес хранилища для формы: сетевая папка как в проводнике и подпапка (сохранённые прежними
// версиями «\\сервер\» + «папка» показываются уже разобранными).
func smbView(share, dir string) map[string]string {
sh, d := smbLocation(share, dir)
if sh == "" {
return map[string]string{"Share": share, "Dir": dir}
}
return map[string]string{"Share": smbWin(sh), "Dir": pickStr(d, "bj-backup")}
}
// smbShareOK — в адресе есть и сервер, и общая папка.
func smbShareOK(share string) bool { return strings.Count(strings.TrimPrefix(share, "//"), "/") == 1 }
@@ -129,10 +139,10 @@ func backupParams(r *http.Request) map[string]string {
}
}
if p["share"] != "" { // адрес как в проводнике → общая папка и путь внутри неё
// подпапка не указана — та же, что у копий по умолчанию (bj-backup): иначе root подставлял папку из
// прежних настроек и «Копии на хранилище» искала не там, куда легла копия
p["share"], p["dir"] = smbLocation(p["share"], p["dir"])
if p["dir"] == "" {
delete(p, "dir")
}
p["dir"] = pickStr(p["dir"], "bj-backup")
}
return p
}
+1
View File
@@ -166,6 +166,7 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
res["depo_db"] = s.Fancy.DepoDB()
res["depo_db_test"], res["depo_db_prod"] = s.NSD.DepoDBTest, s.NSD.DepoDBProd
res["bundled"] = igateBundledVersion()
res["keymedia"] = keymediaState() // missing — ИШ ждёт флешку с ключом: переустановка не поможет
jsonOut(w, res)
})
// установка или обновление ИШ: загруженный архив НРД или версия для скачивания с nsd.ru
+40
View File
@@ -18,6 +18,8 @@ import (
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -45,6 +47,9 @@ type igwWatchState struct {
LastRestart time.Time
LastRemind time.Time
RestartMsg string
// KeyWait — ИШ ждёт флешку с ключом подписи (окно Валидаты «Вставьте ключевой носитель»): перезапуск не
// поможет, пока её нет, а когда она появится — bj перезапустит ИШ сам.
KeyWait bool
}
var igwWatch igwWatchState
@@ -155,6 +160,23 @@ func igwWatchTick(ctx context.Context, rc *RuntimeConfig) {
needAlert := !w.Alerted && down >= igwAlertAfter
canRestart := igwLocal(base) && down >= igwAlertAfter && w.Restarts < igwRestartMax &&
(w.LastRestart.IsZero() || now.Sub(w.LastRestart) >= igwRestartEvery)
keyAlert := false
if igwLocal(base) {
switch keymediaState() {
case "missing": // без флешки ИШ ждёт её в окне Валидаты — перезапуски бесполезны
canRestart = false
if !w.KeyWait {
w.KeyWait, keyAlert = true, true
needAlert = false
w.Alerted, w.LastRemind = true, now
}
case "ok":
if w.KeyWait { // флешку вернули — перезапустить ИШ сразу, попытки — заново
w.KeyWait, w.Restarts, w.LastRestart = false, 0, time.Time{}
canRestart = true
}
}
}
remind := w.Alerted && !canRestart && w.Restarts >= igwRestartMax && now.Sub(w.LastRemind) >= igwRemindEvery
if needAlert {
w.Alerted, w.LastRemind = true, now
@@ -169,6 +191,9 @@ func igwWatchTick(ctx context.Context, rc *RuntimeConfig) {
attempt, since, lastErr := w.Restarts, w.DownSince, w.LastErr
w.mu.Unlock()
if keyAlert {
raiseSystemAlert(rc, "ИШ НРД ждёт флешку с ключом подписи", fmt.Sprintf("Интеграционный шлюз НРД не запускается: флешки с ключом подписи в сервере нет (%s пуста) — Валидата ждёт ключевой носитель. Вставьте флешку с ключом в этот сервер: bj перезапустит шлюз сам. Переустанавливать ИШ не нужно.", keymediaDir))
}
if needAlert {
plan := "ИШ на другом сервере — перезапустите службу ИШ там."
if igwLocal(base) {
@@ -186,6 +211,21 @@ func igwWatchTick(ctx context.Context, rc *RuntimeConfig) {
}
}
// keymediaDir — куда подключается флешка с ключом для ИШ (bj-keymedia-install, строка fstab по UUID).
const keymediaDir = "/var/lib/igate/keymedia"
// keymediaState — флешка с ключом для ИШ: "" — не настроена (ключ не с флешки), "ok" — на месте, "missing" — нет.
func keymediaState() string {
b, err := os.ReadFile("/etc/fstab")
if err != nil || !strings.Contains(string(b), " "+keymediaDir+" ") {
return ""
}
if m, _ := filepath.Glob(keymediaDir + "/vdkeys/*.vdk"); len(m) > 0 {
return "ok"
}
return "missing"
}
// igwTryRestart — перезапуск службы ИШ и проверка, поднялся ли он.
func igwTryRestart(rc *RuntimeConfig, base string, attempt int) {
log.Printf("igw-watch: перезапуск службы %s (попытка %d из %d)", igwServiceUnit, attempt, igwRestartMax)
@@ -583,13 +583,14 @@
<p>Через интеграционный шлюз НРД (ИШ) на этом сервере bj отправляет переводы и получает ответы. bj ставит и настраивает шлюз сам; доступ к тестовому и боевому контуру НРД открывает по заявке на своём сайте — как её подать, ниже.</p>
{{template "igateInstall" .}}
<div class="wiz-info" style="margin:12px 0 0"><span><b>Доступ к контурам НРД</b> — заявкой на сайте НРД, письмами НРД не подключает. Страница сервиса <a href="https://www.nsd.ru/services/novye-servisy/moex-most-dlya-m2m/" target="_blank" rel="noopener">MOEX МОСТ для M2M</a>, раздел «Как подключиться»: заявка на тестирование — система МОСТ, тип «M2M Автотестирование», затем «M2M Подключение»; после тестирования — договор с НРД и боевой контур.</span></div>
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: адрес шлюза (обычно не нужно)</summary>
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Шлюз НРД на другом сервере? (обычно не нужно)</summary>
<p class="muted" style="font-size:13px;margin:8px 0 0">bj работает со шлюзом, который сам поставил на этом сервере: адрес — <code>http://localhost:8090</code>, менять его не нужно. Меняйте, только если у организации уже есть свой интеграционный шлюз НРД на другом сервере и bj должен работать через него: укажите адрес его веб-API (обычно порт 8090). Ключ подписи и каналы тогда настраиваются на том шлюзе.</p>
<form method="post" action="/admin/setup/nsd" style="margin-top:10px;display:grid;gap:10px;max-width:680px">
<input type="hidden" name="return_to" value="/admin/wizard?step=7">
{{if .Settings.NSD.IGWv904}}<input type="hidden" name="igw_v904" value="on">{{end}}
<input type="hidden" name="keep_requisites" value="1">
<input type="hidden" name="profile" value="{{if .Settings.NSD.Profile}}{{.Settings.NSD.Profile}}{{else}}test3-gost{{end}}">
<label style="margin:0">Адрес шлюза на этом сервере
<label style="margin:0">Адрес веб-API шлюза НРД
<input type="text" name="igw_base_url" value="{{if .Settings.NSD.IGWBaseURL}}{{.Settings.NSD.IGWBaseURL}}{{else}}http://localhost:8090{{end}}" placeholder="http://localhost:8090" style="width:100%;margin-top:4px"></label>
<p id="igw-onyx" style="display:none;color:var(--err);font-size:13px;margin:0">Это адрес веб-сервиса НРД (ONYX), а не шлюза: bj работает через ИШ на этом сервере. Укажите адрес шлюза — обычно <a href="#" id="igw-def">http://localhost:8090</a>.</p>
<script>(function(){var u=document.querySelector('[name=igw_base_url]'),w=document.getElementById('igw-onyx');
@@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · Bridge&Join</title>
<style>
/* hidden прячет всегда: иначе display:grid/flex у класса рисовал пустые рамки (полоска под версией ИШ) */
[hidden]{display:none!important}
/* ===================== Дизайн-система ===================== */
/* «Диспетчерская депозитария»: приборная панель оператора НРД/M2M.
Restrained — прохладные нейтрали + один индиго-акцент; MOEX-красный
@@ -9,8 +9,8 @@
<form method="post" action="/admin/setup/backup" id="bk-form" style="display:grid;gap:10px;max-width:640px">
<label class="chk" style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="enabled" style="width:auto"{{if $b.Enabled}} checked{{end}}> <b>Делать копии по расписанию</b></label>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
<label>Сетевая папка — как в проводнике<input name="share" data-need="сетевая папка" value="{{smbWin $b.Share}}" placeholder="\\nas\backup"></label>
<label>Подпапка для копий<input name="dir" value="{{$b.Dir}}" placeholder="bj-backup"></label>
<label>Сетевая папка — как в проводнике<input name="share" data-need="сетевая папка" value="{{if $b.Share}}{{(smbView $b.Share $b.Dir).Share}}{{end}}" placeholder="\\nas\backup"></label>
<label>Подпапка для копий<input name="dir" value="{{if $b.Share}}{{(smbView $b.Share $b.Dir).Dir}}{{end}}" placeholder="bj-backup"></label>
</div>
<p class="muted" style="font-size:12px;margin:-4px 0 0">Например <code>\\10.10.10.11\backup</code> — та же папка, что открывается в проводнике Windows с этим логином. Копии ложатся в подпапку внутри неё.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
@@ -138,8 +138,9 @@
if(!d.installed) head.innerHTML=bad('ИШ на этом сервере не установлен');
else head.innerHTML=(running?ok('ИШ '+d.version+' установлен и работает'):bad('ИШ '+d.version+' установлен, но не работает: движок '+(d.engine||'не отвечает')+', служба '+d.service));
if(d.busy) head.innerHTML='<span style="color:var(--accent)">⟳ Шлюз перезапускается — '+esc(d.busy)+'; состояние — последнее известное</span>';
if(!running && d.keymedia==='missing'){ head.innerHTML='<span style="color:var(--warn)">⚠ ИШ '+esc(d.version)+' ждёт флешку с ключом подписи — её нет в сервере. Вставьте флешку: bj перезапустит шлюз сам. Переустанавливать ИШ не нужно.</span>'; }
// не стоит или не работает — одна кнопка: поставить заново проверенную версию с сервера (систему переставлять не нужно)
ri.style.display=running?'none':'block';
ri.style.display=(running||d.keymedia==='missing')?'none':'block'; // без флешки переустановка не поможет
if(!running){
if(d.bundled){ bb.style.display=''; bb.textContent=d.installed?'Установить ИШ заново':'Установить ИШ'; bv.textContent='версия '+d.bundled+' — проверена ZETIT, дистрибутив на сервере'; }
else { bb.style.display='none'; bv.textContent='Дистрибутива ИШ на сервере нет — он приходит с обновлением bj («Настройка → Обновления»).'; }
@@ -13,6 +13,13 @@
.kw-tile b{display:block;font-size:14.5px}
.kw-tile small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:3px}
.kw-panel{margin-top:16px}
.kw-done{display:flex;gap:14px;align-items:flex-start;padding:16px 18px;margin-top:4px;border-radius:14px;background:color-mix(in srgb,var(--ok) 9%,var(--card));border:1px solid color-mix(in srgb,var(--ok) 32%,transparent)}
.kw-done-ic{flex:none;width:30px;height:30px;border-radius:50%;background:var(--ok);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px}
.kw-done-b b{display:block;font-size:15.5px;margin-top:4px}
.kw-done-b p{margin:6px 0 0;font-size:14px;line-height:1.5}
.kw-done-act{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px}
.kw-link{background:none;border:0;padding:0;color:var(--accent);font:inherit;font-size:13.5px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
.kw-howto{margin:0 0 12px}.kw-howto summary{cursor:pointer;color:var(--accent);font-size:13.5px;margin-bottom:8px}
.kw-drive{display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;cursor:pointer;margin:0}
.kw-pad{height:200px;margin-top:12px;border:2px dashed var(--border);border-radius:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:0 16px;color:var(--muted);user-select:none;-webkit-user-select:none;touch-action:none;cursor:default}
.kw-pad.on{border-color:var(--accent);background:var(--accent-weak);color:var(--accent);cursor:crosshair}
@@ -83,6 +90,7 @@
<div id="kw-main">
{{/* ===== Лента шагов — когда путь выбран (флешка с ключом или новая флешка из архива) ===== */}}
{{if or $s.StagingID $s.Done}}
{{if $s.Done}}<details class="kw-howto"><summary>Как прошла установка — {{len $s.Steps}} этапов</summary>{{end}}
<div class="card">
<ol style="list-style:none;padding:0;margin:0;display:grid;gap:12px">
{{range $i, $step := $s.Steps}}
@@ -102,22 +110,21 @@
{{end}}
</ol>
</div>
{{if $s.Done}}</details>{{end}}
{{/* ===== Действие в зависимости от состояния ===== */}}
{{end}}
{{if $s.Done}}
<div class="card" style="background:var(--ok-weak)">
<h2>✓ Готово</h2>
<p>{{if $s.MediaDevice}}Ключ с флешки подключён к шлюзу НРД как есть и подписывает — флешка не менялась.{{else}}Ключ записан на флешку, справочник сертификатов сформирован, ключ подписывает.{{end}}</p>
{{if $s.Backup}}<p class="muted">Бэкап прежнего носителя: <code>{{$s.Backup}}</code></p>{{end}}
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
{{if devOn}}<form method="post" action="/admin/setup/test-nsd" style="margin:0">
<input type="hidden" name="scenario" value="2001">
<button type="submit" class="btn btn-ok">→ Отправить тестовый документ роботу</button>
</form>{{else if not .InWizard}}<a href="/admin/wizard?step=8" class="btn btn-ok">→ Вернуться в мастер настройки</a> <a href="/admin/status" class="btn btn-secondary">Проверить подключения</a>{{end}}
<form method="post" action="/admin/setup/keywizard/reset" style="margin:0">{{template "keyWizRet" .}}
<button type="submit" class="btn btn-secondary">Установить ещё один ключ</button>
</form>
<div class="kw-done">
<span class="kw-done-ic" aria-hidden="true">✓</span>
<div class="kw-done-b">
<b>Ключ подключён и подписывает</b>
<p>{{if $s.MediaDevice}}Флешка с ключом подключена к шлюзу НРД как есть — на неё ничего не записывалось.{{else}}Ключ записан на флешку, справочник сертификатов сформирован, шлюз НРД подписывает им документы.{{end}} Флешка должна оставаться в сервере: без неё шлюз не запустится.</p>
{{if $s.Backup}}<p class="muted" style="font-size:12.5px">Копия прежнего содержимого флешки: <code>{{$s.Backup}}</code></p>{{end}}
<div class="kw-done-act">
{{if devOn}}<form method="post" action="/admin/setup/test-nsd" style="margin:0"><input type="hidden" name="scenario" value="2001"><button type="submit" class="btn btn-ok">→ Отправить тестовый документ роботу</button></form>{{else if not .InWizard}}<a href="/admin/wizard?step=8" class="btn btn-ok">→ Вернуться в мастер настройки</a> <a href="/admin/status" class="btn btn-secondary">Проверить подключения</a>{{end}}
<form method="post" action="/admin/setup/keywizard/reset" style="margin:0">{{template "keyWizRet" .}}<button type="submit" class="kw-link" title="Когда истечёт сертификат или понадобится ключ другого контура">Заменить ключ</button></form>
</div>
</div>
</div>
{{else if and $s.StagingID $s.MediaDevice}}