Проверка на чистой ВМ: служба впервые установленного модуля включается в автозапуск, даже если unit-файл уже положил образ (иначе bj-crypto после перезагрузки не стартовал); bj-crypto в готовности — предупреждение, а не блокер (пакеты для НРД подписывает шлюз), до установки модуля не проверяется; в мастере без ссылки «Открыть мастер» на самом мастере, без двойного заголовка «Наша организация», без QUIK и DEPO в текстах и маршрутах уведомлений, если их нет в лицензии; «162 участника», понятная сводка готовности; адрес SMTP через JoinHostPort
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -833,13 +833,13 @@ func nowPage(title, active string) page {
|
|||||||
switch {
|
switch {
|
||||||
case s.NSD.IGWBaseURL == "":
|
case s.NSD.IGWBaseURL == "":
|
||||||
p.IsMockMode = true
|
p.IsMockMode = true
|
||||||
p.MockReason = "ИШ НРД не настроен — обмен с НРД не выполняется"
|
p.MockReason = "Не подключён интеграционный шлюз НРД."
|
||||||
if p.DevMode {
|
if p.DevMode {
|
||||||
p.MockReason = "ИШ НРД не настроен — заявки идут через внутренний mock (Decision эмитируется через 3 сек)"
|
p.MockReason = "ИШ НРД не настроен — заявки идут через внутренний mock (Decision эмитируется через 3 сек)"
|
||||||
}
|
}
|
||||||
case s.Crypto.Provider == "" || s.Crypto.Provider == "stub":
|
case s.Crypto.Provider == "" || s.Crypto.Provider == "stub":
|
||||||
p.IsMockMode = true
|
p.IsMockMode = true
|
||||||
p.MockReason = "Не настроено средство подписи (СКЗИ) — обмен с НРД невозможен"
|
p.MockReason = "Не настроено средство подписи (СКЗИ)."
|
||||||
if p.DevMode {
|
if p.DevMode {
|
||||||
p.MockReason = "Провайдер СКЗИ = stub — подпись не делается, реальный обмен с НРД невозможен"
|
p.MockReason = "Провайдер СКЗИ = stub — подпись не делается, реальный обмен с НРД невозможен"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"mime"
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -64,7 +65,7 @@ func sendSMTP(s SMTPSettings, subject, body string) error {
|
|||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = 587
|
port = 587
|
||||||
}
|
}
|
||||||
addr := fmt.Sprintf("%s:%d", s.Host, port)
|
addr := net.JoinHostPort(s.Host, strconv.Itoa(port))
|
||||||
msg := buildMIME(s.From, rcpts, subject, body)
|
msg := buildMIME(s.From, rcpts, subject, body)
|
||||||
|
|
||||||
dialer := &net.Dialer{Timeout: 15 * time.Second}
|
dialer := &net.Dialer{Timeout: 15 * time.Second}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -236,3 +237,33 @@ func TestLettersFill(t *testing.T) {
|
|||||||
t.Error("неизвестное письмо")
|
t.Error("неизвестное письмо")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRuPlural(t *testing.T) {
|
||||||
|
for n, want := range map[int]string{1: "участник", 2: "участника", 4: "участника", 5: "участников", 11: "участников", 12: "участников", 21: "участник", 162: "участника", 111: "участников", 0: "участников"} {
|
||||||
|
if got := ruPlural(n, "участник", "участника", "участников"); got != want {
|
||||||
|
t.Errorf("%d: %q, ожидалось %q", n, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Без лицензии QUIK МНП маршрута «ошибки QUIK» в форме нет, а сохранение формы не стирает его прежнее значение.
|
||||||
|
func TestNotifyKindsByLicense(t *testing.T) {
|
||||||
|
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
||||||
|
for _, k := range visibleNotifyKinds(rc) {
|
||||||
|
if k.Key == "quik" {
|
||||||
|
t.Fatal("без лицензии QUIK МНП маршрута QUIK быть не должно")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(visibleNotifyKinds(rc)) != len(notifyKinds)-1 {
|
||||||
|
t.Fatalf("типы: %+v", visibleNotifyKinds(rc))
|
||||||
|
}
|
||||||
|
h := &setupHandlers{rc: rc}
|
||||||
|
form := url.Values{"enabled": {"on"}, "emails": {"depo@example.ru"}, "route_incoming_depo": {"1"}}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/admin/setup/incoming-notify", strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
h.saveIncomingNotify(httptest.NewRecorder(), req)
|
||||||
|
r := rc.Snapshot().Incoming.Routes
|
||||||
|
if r["incoming"] != "depo" || r["quik"] != defaultNotifyRoutes["quik"] {
|
||||||
|
t.Fatalf("маршруты после сохранения: %+v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,6 +19,23 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// cryptoJarPath — модуль службы пробной подписи bj-crypto (компонент crypto-service.jar).
|
||||||
|
const cryptoJarPath = "/opt/bj/crypto-service.jar"
|
||||||
|
|
||||||
|
// ruPlural — форма слова для числа: 1 участник, 2 участника, 5 участников.
|
||||||
|
func ruPlural(n int, one, few, many string) string {
|
||||||
|
n %= 100
|
||||||
|
switch {
|
||||||
|
case n >= 11 && n <= 14:
|
||||||
|
return many
|
||||||
|
case n%10 == 1:
|
||||||
|
return one
|
||||||
|
case n%10 >= 2 && n%10 <= 4:
|
||||||
|
return few
|
||||||
|
}
|
||||||
|
return many
|
||||||
|
}
|
||||||
|
|
||||||
// ReadyCheck — одна строка итоговой проверки.
|
// ReadyCheck — одна строка итоговой проверки.
|
||||||
type ReadyCheck struct {
|
type ReadyCheck struct {
|
||||||
Order int `json:"-"`
|
Order int `json:"-"`
|
||||||
@@ -107,16 +125,22 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
|||||||
cctx, cancel := context.WithTimeout(ctx, 4*time.Second)
|
cctx, cancel := context.WithTimeout(ctx, 4*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
ver, _, _ := validataVersion(cctx)
|
ver, _, _ := validataVersion(cctx)
|
||||||
|
c := ReadyCheck{Order: 6, Title: "СКЗИ «Валидата»", Link: "/admin/wizard?step=6", Required: true}
|
||||||
|
if ver == "" {
|
||||||
|
c.State, c.Detail = "err", "Валидата на сервере не найдена — без неё шлюз не подпишет пакеты для НРД"
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
c.State, c.Detail = "ok", "Валидата "+ver
|
||||||
|
// Служба bj-crypto — пробная подпись и проверка ключа из bj; пакеты для НРД подписывает шлюз, поэтому
|
||||||
|
// её остановка обмену не мешает. Модуль приезжает с обновлениями — до них службы нет, это не ошибка.
|
||||||
|
if _, err := os.Stat(cryptoJarPath); err == nil {
|
||||||
out, _ := exec.CommandContext(cctx, "systemctl", "is-active", "bj-crypto.service").Output()
|
out, _ := exec.CommandContext(cctx, "systemctl", "is-active", "bj-crypto.service").Output()
|
||||||
svc := strings.TrimSpace(string(out))
|
if svc := strings.TrimSpace(string(out)); svc != "active" {
|
||||||
c := ReadyCheck{Order: 6, Title: "СКЗИ «Валидата» и служба криптографии", Link: "/admin/wizard?step=6", Required: true}
|
c.State, c.Link = "warn", "/admin/setup#sec-crypto"
|
||||||
switch {
|
c.Detail += "; служба пробной подписи bj-crypto не запущена (" + pickStr(svc, "нет") + ") — обмену не мешает: пакеты для НРД подписывает шлюз"
|
||||||
case ver == "":
|
} else {
|
||||||
c.State, c.Detail = "err", "Валидата на сервере не найдена"
|
c.Detail += ", служба пробной подписи bj-crypto работает"
|
||||||
case svc != "active":
|
}
|
||||||
c.State, c.Detail = "err", "Валидата "+ver+", но служба bj-crypto не запущена ("+pickStr(svc, "нет")+")"
|
|
||||||
default:
|
|
||||||
c.State, c.Detail = "ok", "Валидата "+ver+", служба bj-crypto работает"
|
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
@@ -176,7 +200,11 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
|||||||
case s.Alerts.Zabbix.Enabled:
|
case s.Alerts.Zabbix.Enabled:
|
||||||
c.State, c.Detail = "ok", "Zabbix "+s.Alerts.Zabbix.Server
|
c.State, c.Detail = "ok", "Zabbix "+s.Alerts.Zabbix.Server
|
||||||
default:
|
default:
|
||||||
c.State, c.Detail = "warn", "не настроены: о сбоях (НРД не отвечает, недоставленные переводы, ошибки DEPO, срок сертификата) узнают только открыв bj"
|
depo := ""
|
||||||
|
if moduleAllowed(rc, "quik-mnp") {
|
||||||
|
depo = ", ошибки DEPO"
|
||||||
|
}
|
||||||
|
c.State, c.Detail = "warn", "не настроены: о сбоях (НРД не отвечает, недоставленные переводы"+depo+", срок сертификата) узнают только открыв bj"
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
@@ -241,9 +269,9 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
|||||||
case len(hb.Participants) == 0:
|
case len(hb.Participants) == 0:
|
||||||
c.State, c.Detail = "err", "справочника нет — получателя перевода не определить"
|
c.State, c.Detail = "err", "справочника нет — получателя перевода не определить"
|
||||||
case !at.IsZero() && time.Since(at) > 14*24*time.Hour:
|
case !at.IsZero() && time.Since(at) > 14*24*time.Hour:
|
||||||
c.State, c.Detail = "warn", fmt.Sprintf("%d участников, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), at.Format("02.01.2006"))
|
c.State, c.Detail = "warn", fmt.Sprintf("%d %s, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), ruPlural(len(hb.Participants), "участник", "участника", "участников"), at.Format("02.01.2006"))
|
||||||
default:
|
default:
|
||||||
c.State, c.Detail = "ok", fmt.Sprintf("%d участников, от %s", len(hb.Participants), at.Format("02.01.2006"))
|
c.State, c.Detail = "ok", fmt.Sprintf("%d %s, от %s", len(hb.Participants), ruPlural(len(hb.Participants), "участник", "участника", "участников"), at.Format("02.01.2006"))
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -602,6 +602,18 @@ var notifyKinds = []struct{ Key, Title string }{
|
|||||||
{"system", "Прочие системные оповещения"},
|
{"system", "Прочие системные оповещения"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// visibleNotifyKinds — типы уведомлений этой установки: ошибки QUIK МНП — только при модуле в лицензии.
|
||||||
|
func visibleNotifyKinds(rc *RuntimeConfig) []struct{ Key, Title string } {
|
||||||
|
var out []struct{ Key, Title string }
|
||||||
|
for _, k := range notifyKinds {
|
||||||
|
if k.Key == "quik" && rc != nil && !moduleAllowed(rc, "quik-mnp") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// defaultNotifyRoutes — маршруты по умолчанию, пока оператор не настроил свои.
|
// defaultNotifyRoutes — маршруты по умолчанию, пока оператор не настроил свои.
|
||||||
var defaultNotifyRoutes = map[string]string{"incoming": "depo,back", "orders": "depo,back", "quik": "back,middle", "system": "back"}
|
var defaultNotifyRoutes = map[string]string{"incoming": "depo,back", "orders": "depo,back", "quik": "back,middle", "system": "back"}
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
if (step >= 3 && step <= 5) || step == 9 || step == 11 { // DEPO, организация, QUIK МНП, оповещения и копии, модули — общие блоки из «Настройки»
|
if (step >= 3 && step <= 5) || step == 9 || step == 11 { // DEPO, организация, QUIK МНП, оповещения и копии, модули — общие блоки из «Настройки»
|
||||||
fd := h.setupData(w, r, "")
|
fd := h.setupData(w, r, "")
|
||||||
|
fd.InWizard = true
|
||||||
d.Fancy = &fd
|
d.Fancy = &fd
|
||||||
}
|
}
|
||||||
if step == 3 {
|
if step == 3 {
|
||||||
@@ -458,6 +459,7 @@ func (h *setupHandlers) testSignCrypto(w http.ResponseWriter, r *http.Request) {
|
|||||||
type SetupData struct {
|
type SetupData struct {
|
||||||
page
|
page
|
||||||
FromWizard bool // раздел открыт из мастера (настройка модуля) — показать «Вернуться в мастер»
|
FromWizard bool // раздел открыт из мастера (настройка модуля) — показать «Вернуться в мастер»
|
||||||
|
InWizard bool // общий блок показан внутри шага мастера — заголовок и пояснение даёт сам шаг
|
||||||
Settings Settings
|
Settings Settings
|
||||||
Suggest *DepoRequisites // предложение реквизитов из DEPO
|
Suggest *DepoRequisites // предложение реквизитов из DEPO
|
||||||
Readiness []Readiness
|
Readiness []Readiness
|
||||||
@@ -554,7 +556,7 @@ func (h *setupHandlers) setupData(w http.ResponseWriter, req *http.Request, flas
|
|||||||
OrgName: orgName(s),
|
OrgName: orgName(s),
|
||||||
OrgCode: orgCode(s),
|
OrgCode: orgCode(s),
|
||||||
NotifyGroups: notifyGroups,
|
NotifyGroups: notifyGroups,
|
||||||
NotifyKinds: notifyKinds,
|
NotifyKinds: visibleNotifyKinds(h.rc),
|
||||||
DepoJob: depoJobView(s.Fancy),
|
DepoJob: depoJobView(s.Fancy),
|
||||||
DepoTables: depoTablesView(s.Fancy),
|
DepoTables: depoTablesView(s.Fancy),
|
||||||
QuikExport: quikExportView(h.rc, req.URL.Query().Get("reveal_ro") == "1"),
|
QuikExport: quikExportView(h.rc, req.URL.Query().Get("reveal_ro") == "1"),
|
||||||
@@ -1103,7 +1105,14 @@ func (h *setupHandlers) saveIncomingNotify(w http.ResponseWriter, r *http.Reques
|
|||||||
EmailsMiddle: strings.TrimSpace(r.FormValue("emails_middle")),
|
EmailsMiddle: strings.TrimSpace(r.FormValue("emails_middle")),
|
||||||
Routes: map[string]string{},
|
Routes: map[string]string{},
|
||||||
}
|
}
|
||||||
for _, k := range notifyKinds {
|
prev := h.rc.Snapshot().Incoming.Routes
|
||||||
|
if prev == nil {
|
||||||
|
prev = defaultNotifyRoutes
|
||||||
|
}
|
||||||
|
for _, k := range notifyKinds { // скрытые в форме типы (модуль не в лицензии) — прежний маршрут
|
||||||
|
s.Routes[k.Key] = prev[k.Key]
|
||||||
|
}
|
||||||
|
for _, k := range visibleNotifyKinds(h.rc) {
|
||||||
var gs []string
|
var gs []string
|
||||||
for _, g := range notifyGroups {
|
for _, g := range notifyGroups {
|
||||||
if r.FormValue("route_"+k.Key+"_"+g.Key) != "" {
|
if r.FormValue("route_"+k.Key+"_"+g.Key) != "" {
|
||||||
|
|||||||
@@ -316,8 +316,8 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
|||||||
{{if eq .Step 4}}
|
{{if eq .Step 4}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2><span class="dot {{if .Done.Org}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
<h2><span class="dot {{if .Done.Org}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
||||||
<div class="help-block"><strong>Что это.</strong> Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: код участника, ИНН и наименование попадают в пакеты НРД и в поручения QUIK.<br>
|
<div class="help-block"><strong>Что это.</strong> Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: код участника, ИНН и наименование попадают в пакеты НРД{{if .DepoLicensed}} и в поручения QUIK{{end}}.<br>
|
||||||
<strong>Откуда взять.</strong> Если подключена база DEPO (раздел «Учёт DEPO»), bj сам определяет реквизиты по учёту — проверьте их и сохраните. Иначе выберите себя из справочника участников НРД или введите вручную.</div>
|
<strong>Откуда взять.</strong> {{if .DepoLicensed}}Если подключена база DEPO (раздел «Учёт DEPO»), bj сам определяет реквизиты по учёту — проверьте их и сохраните. Иначе выберите{{else}}Выберите{{end}} себя из справочника участников НРД или введите вручную.</div>
|
||||||
{{if .Suggest}}{{if .Suggest.OrgName}}{{if not .Done.Org}}<p style="margin:8px 0;padding:8px 12px;border-radius:8px;background:var(--accent-weak)">✓ <strong>Определено из DEPO</strong>: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}}{{if .Suggest.AccountID}}, счёт НРД {{.Suggest.AccountID}}{{end}}. Поля ниже заполнены — проверьте и нажмите «Сохранить».</p>{{end}}{{end}}{{end}}
|
{{if .Suggest}}{{if .Suggest.OrgName}}{{if not .Done.Org}}<p style="margin:8px 0;padding:8px 12px;border-radius:8px;background:var(--accent-weak)">✓ <strong>Определено из DEPO</strong>: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}}{{if .Suggest.AccountID}}, счёт НРД {{.Suggest.AccountID}}{{end}}. Поля ниже заполнены — проверьте и нажмите «Сохранить».</p>{{end}}{{end}}{{end}}
|
||||||
{{with .Fancy}}{{template "orgSection" .}}{{end}}
|
{{with .Fancy}}{{template "orgSection" .}}{{end}}
|
||||||
<div class="card" id="nsd-req" style="margin-top:12px">
|
<div class="card" id="nsd-req" style="margin-top:12px">
|
||||||
@@ -517,7 +517,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
|||||||
{{if eq .Step 9}}
|
{{if eq .Step 9}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2><span class="dot {{if .Done.Alerts}}ok{{else}}warn{{end}}"></span>Оповещения и резервные копии</h2>
|
<h2><span class="dot {{if .Done.Alerts}}ok{{else}}warn{{end}}"></span>Оповещения и резервные копии</h2>
|
||||||
<div class="help-block"><strong>Зачем.</strong> bj сам следит за обменом: НРД не отвечает, перевод не доставлен, ошибка обработки в DEPO, истекает сертификат ключа. Чтобы об этом узнали сразу, а не открыв bj, — укажите почту. Резервные копии позволят восстановить сервер целиком (настройки, заявки, шлюз НРД) после сбоя или переустановки.<br>
|
<div class="help-block"><strong>Зачем.</strong> bj сам следит за обменом: НРД не отвечает, перевод не доставлен{{if .DepoLicensed}}, ошибка обработки в DEPO{{end}}, истекает сертификат ключа. Чтобы об этом узнали сразу, а не открыв bj, — укажите почту. Резервные копии позволят восстановить сервер целиком (настройки, заявки, шлюз НРД) после сбоя или переустановки.<br>
|
||||||
<strong>Можно позже.</strong> Шаг необязательный — обмен работает и без него, но мы настоятельно советуем настроить оба пункта.</div>
|
<strong>Можно позже.</strong> Шаг необязательный — обмен работает и без него, но мы настоятельно советуем настроить оба пункта.</div>
|
||||||
<style>.wiz9 label:not(:has(input[type=checkbox])){display:block;margin:0}.wiz9 label input:not([type=checkbox]),.wiz9 label select,.wiz9 .grid input,.wiz9 .grid select{width:100%;box-sizing:border-box;margin-top:4px}</style>
|
<style>.wiz9 label:not(:has(input[type=checkbox])){display:block;margin:0}.wiz9 label input:not([type=checkbox]),.wiz9 label select,.wiz9 .grid input,.wiz9 .grid select{width:100%;box-sizing:border-box;margin-top:4px}</style>
|
||||||
<div class="wiz9">
|
<div class="wiz9">
|
||||||
@@ -568,11 +568,12 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
|||||||
var tb=document.querySelector('#rd-tab tbody'), sum=document.getElementById('rd-sum'), dot=document.getElementById('rd-dot');
|
var tb=document.querySelector('#rd-tab tbody'), sum=document.getElementById('rd-sum'), dot=document.getElementById('rd-dot');
|
||||||
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||||
var ic={ok:'<span style="color:var(--ok)">✓</span>',warn:'<span style="color:var(--warn)">⚠</span>',err:'<span style="color:var(--err)">✗</span>',skip:'<span class="muted">—</span>'};
|
var ic={ok:'<span style="color:var(--ok)">✓</span>',warn:'<span style="color:var(--warn)">⚠</span>',err:'<span style="color:var(--err)">✗</span>',skip:'<span class="muted">—</span>'};
|
||||||
|
function pl(n,one,few,many){var m=n%100,d=n%10;return (m>=11&&m<=14)?many:d===1?one:(d>=2&&d<=4)?few:many}
|
||||||
function load(){ sum.textContent='Проверяю…'; tb.innerHTML='';
|
function load(){ sum.textContent='Проверяю…'; tb.innerHTML='';
|
||||||
fetch('/admin/setup/readiness',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
fetch('/admin/setup/readiness',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
||||||
tb.innerHTML=d.checks.map(function(c){return '<tr><td style="width:24px;vertical-align:top">'+ic[c.state]+'</td><td style="vertical-align:top;white-space:nowrap;padding-right:10px"><b>'+esc(c.title)+'</b></td><td style="vertical-align:top">'+esc(c.detail)+(c.state!=='ok'&&c.link?' <a href="'+esc(c.link)+'">Исправить →</a>':'')+'</td></tr>'}).join('');
|
tb.innerHTML=d.checks.map(function(c){return '<tr><td style="width:24px;vertical-align:top">'+ic[c.state]+'</td><td style="vertical-align:top;white-space:nowrap;padding-right:10px"><b>'+esc(c.title)+'</b></td><td style="vertical-align:top">'+esc(c.detail)+(c.state!=='ok'&&c.link?' <a href="'+esc(c.link)+'">Исправить →</a>':'')+'</td></tr>'}).join('');
|
||||||
var red=d.checks.filter(function(c){return c.state==='err'}).length, yel=d.checks.filter(function(c){return c.state==='warn'}).length;
|
var red=d.checks.filter(function(c){return c.state==='err'}).length, yel=d.checks.filter(function(c){return c.state==='warn'}).length;
|
||||||
sum.innerHTML=d.ready?(yel?'<span style="color:var(--ok)">✓ Обмен с НРД готов к работе</span> <span class="muted" style="font-weight:400">— желательно исправить: '+yel+'</span>':'<span style="color:var(--ok)">✓ Всё готово к работе</span>'):'<span style="color:var(--err)">✗ Не готово: обязательных проблем — '+red+'</span>';
|
sum.innerHTML=d.ready?(yel?'<span style="color:var(--ok)">✓ Обмен с НРД готов к работе</span> <span class="muted" style="font-weight:400">— желательно исправить ещё '+yel+' '+pl(yel,'пункт','пункта','пунктов')+' (отмечены ⚠)</span>':'<span style="color:var(--ok)">✓ Всё готово к работе</span>'):'<span style="color:var(--err)">✗ Пока не готово: осталось '+red+' '+pl(red,'обязательный пункт','обязательных пункта','обязательных пунктов')+' (отмечены ✗)</span>';
|
||||||
if(dot) dot.className='dot '+(d.ready?(yel?'warn':'ok'):'err');
|
if(dot) dot.className='dot '+(d.ready?(yel?'warn':'ok'):'err');
|
||||||
document.getElementById('rd-at').textContent='проверено '+d.at;
|
document.getElementById('rd-at').textContent='проверено '+d.at;
|
||||||
}).catch(function(){ sum.textContent='Проверка не выполнилась — обновите страницу.'; }); }
|
}).catch(function(){ sum.textContent='Проверка не выполнилась — обновите страницу.'; }); }
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
|
|||||||
<div class="banner-mock">
|
<div class="banner-mock">
|
||||||
<span class="dot warn" style="width:9px;height:9px;margin:0"></span>
|
<span class="dot warn" style="width:9px;height:9px;margin:0"></span>
|
||||||
<div><strong style="color:var(--warn)">{{if devOn}}Режим эмуляции{{else}}Обмен с НРД не настроен{{end}}</strong> — {{if devOn}}реального обмена с НРД нет{{else}}поручения в НРД не отправляются{{end}}. <span class="muted">{{.MockReason}}</span></div>
|
<div><strong style="color:var(--warn)">{{if devOn}}Режим эмуляции{{else}}Обмен с НРД не настроен{{end}}</strong> — {{if devOn}}реального обмена с НРД нет{{else}}поручения в НРД не отправляются{{end}}. <span class="muted">{{.MockReason}}</span></div>
|
||||||
<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>
|
{{if ne .Active "wizard"}}<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{{/* «Наша организация» — одна и та же в «Настройка → НРД» и в мастере настройки (данные — SetupData). */}}
|
{{/* «Наша организация» — одна и та же в «Настройка → НРД» и в мастере настройки (данные — SetupData). */}}
|
||||||
{{define "orgSection"}}
|
{{define "orgSection"}}
|
||||||
<div class="card" id="sec-org">
|
<div class="card" id="sec-org">
|
||||||
<h2><span class="dot {{if and .OrgINN .OrgCode}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
{{if not .InWizard}}<h2><span class="dot {{if and .OrgINN .OrgCode}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
||||||
<p class="muted">Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах, код отправителя и код фирмы в QUIK. Выберите себя из справочника участников НРД или введите данные вручную.</p>
|
<p class="muted">Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах{{if mod "quik-mnp"}}, код отправителя и код фирмы в QUIK{{end}}. Выберите себя из справочника участников НРД или введите данные вручную.</p>{{end}}
|
||||||
{{if and .OrgINN .OrgCode}}<p style="margin:8px 0 0"><b>{{if .OrgName}}{{.OrgName}}{{else}}—{{end}}</b> · ИНН <code>{{.OrgINN}}</code> · код <code>{{.OrgCode}}</code>{{if not .Settings.NSD.OrgINN}} <span class="muted">(ИНН взят из справочника по коду)</span>{{end}}</p>
|
{{if and .OrgINN .OrgCode}}<p style="margin:8px 0 0"><b>{{if .OrgName}}{{.OrgName}}{{else}}—{{end}}</b> · ИНН <code>{{.OrgINN}}</code> · код <code>{{.OrgCode}}</code>{{if not .Settings.NSD.OrgINN}} <span class="muted">(ИНН взят из справочника по коду)</span>{{end}}</p>
|
||||||
{{else}}<p style="margin:8px 0 0;color:var(--err)">Организация не задана — переводы не будут сформированы.</p>{{end}}
|
{{else}}<p style="margin:8px 0 0;color:var(--err)">Организация не задана — переводы не будут сформированы.</p>{{end}}
|
||||||
<form method="post" action="/admin/setup/org" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
|
<form method="post" action="/admin/setup/org" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type InstallResult struct {
|
|||||||
Release string `json:"release,omitempty"`
|
Release string `json:"release,omitempty"`
|
||||||
Installed []map[string]string `json:"installed,omitempty"`
|
Installed []map[string]string `json:"installed,omitempty"`
|
||||||
Restarted []string `json:"restarted,omitempty"`
|
Restarted []string `json:"restarted,omitempty"`
|
||||||
|
Enabled []string `json:"enabled,omitempty"` // службы, включённые в автозапуск (впервые установленные модули)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallStaged — установить компоненты из каталога подготовки: manifest.json (подписанный),
|
// InstallStaged — установить компоненты из каталога подготовки: manifest.json (подписанный),
|
||||||
@@ -129,6 +130,10 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
|
|||||||
ts := time.Now().Format("20060102-150405")
|
ts := time.Now().Format("20060102-150405")
|
||||||
var units, newUnits []string
|
var units, newUnits []string
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
|
// Модуля на сервере ещё не было — его службу включаем, даже если unit-файл уже стоит: установка
|
||||||
|
// с образа кладёт unit-файлы всех служб сразу (выключенными), а файл модуля приезжает обновлением.
|
||||||
|
_, statErr := os.Stat(j.c.PathAt(opt.Root))
|
||||||
|
fresh := statErr != nil
|
||||||
if err := installFile(j.file, j.c.PathAt(opt.Root), j.c.Exec, ts); err != nil {
|
if err := installFile(j.file, j.c.PathAt(opt.Root), j.c.Exec, ts); err != nil {
|
||||||
return fail("установка %s: %v", j.c.Name, err)
|
return fail("установка %s: %v", j.c.Name, err)
|
||||||
}
|
}
|
||||||
@@ -140,7 +145,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fail("служба %s: %v", j.c.Unit, err)
|
return fail("служба %s: %v", j.c.Unit, err)
|
||||||
}
|
}
|
||||||
if added {
|
if added || fresh {
|
||||||
newUnits = append(newUnits, j.c.Unit)
|
newUnits = append(newUnits, j.c.Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +158,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
|
|||||||
return fail("включение служб %s: %v: %s", strings.Join(newUnits, ", "), err, strings.TrimSpace(string(out)))
|
return fail("включение служб %s: %v: %s", strings.Join(newUnits, ", "), err, strings.TrimSpace(string(out)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
res.Enabled = newUnits
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
if j.c.Name == "bj-installer" && opt.Root == "" {
|
if j.c.Name == "bj-installer" && opt.Root == "" {
|
||||||
// новые службы и скрипты из обновлённого установщика; перезапуск служб — отдельно ниже
|
// новые службы и скрипты из обновлённого установщика; перезапуск служб — отдельно ниже
|
||||||
|
|||||||
@@ -66,6 +66,29 @@ func TestInstallSplitModuleWithUnit(t *testing.T) {
|
|||||||
if inst := LoadInstalled(filepath.Join(root, InstalledFile)); inst["bj-npr"].Version != "0.1.0" {
|
if inst := LoadInstalled(filepath.Join(root, InstalledFile)); inst["bj-npr"].Version != "0.1.0" {
|
||||||
t.Fatalf("components.json: %+v", inst)
|
t.Fatalf("components.json: %+v", inst)
|
||||||
}
|
}
|
||||||
|
if len(res.Enabled) != 1 || res.Enabled[0] != "bj-npr" {
|
||||||
|
t.Fatalf("служба впервые установленного модуля должна включаться: %+v", res.Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Повторная установка того же модуля: служба уже стоит и модуль был — автозапуск не трогаем
|
||||||
|
// (администратор мог выключить службу сам).
|
||||||
|
res = InstallStaged(stageNPR(t, priv, `{"names":["bj-npr"]}`), InstallOptions{Root: root, PubKey: pub, Units: units})
|
||||||
|
if !res.OK || len(res.Enabled) != 0 {
|
||||||
|
t.Fatalf("повторная установка не должна включать службу: %+v", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Установка с образа: unit-файл уже положен (выключенным), файла модуля нет — служба включается.
|
||||||
|
root2 := t.TempDir()
|
||||||
|
if err := os.MkdirAll(filepath.Join(root2, "/etc/systemd/system"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root2, "/etc/systemd/system/bj-npr.service"), []byte("[Service]\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res = InstallStaged(stageNPR(t, priv, `{"names":["bj-npr"],"new":["bj-npr"]}`), InstallOptions{Root: root2, PubKey: pub, Units: units})
|
||||||
|
if !res.OK || len(res.Enabled) != 1 || res.Enabled[0] != "bj-npr" {
|
||||||
|
t.Fatalf("unit-файл из образа: служба должна включиться при первой установке модуля: %+v", res)
|
||||||
|
}
|
||||||
|
|
||||||
// Самостоятельный модуль (не FromCore) по new не ставится.
|
// Самостоятельный модуль (не FromCore) по new не ставится.
|
||||||
c, _ := ComponentByName("bj-lk")
|
c, _ := ComponentByName("bj-lk")
|
||||||
|
|||||||
Reference in New Issue
Block a user