Снимок DEPO для bj; доменное имя сервера — и в настройках
depo_snapshot.go: объекты FTV_M2M*/bj* (тип, даты создания/изменения, хеш текста, если доступен), столбцы и строки таблиц, счётчики по STATE, зависимости поставки Fancy, константы процедур, задания SQL Agent, права логина bj. Данных клиентов нет. Раз в сутки и по кнопке в «Статусе системы» (GET /admin/status/depo-snapshot), история в /var/lib/bj/.bj/depo-snapshots — сравнить установки (28 и 9) и увидеть ручные правки в DEPO. «Сервер: HTTPS» — поле «Доменное имя сервера bj» (было только в мастере). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
@@ -0,0 +1,304 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
// depo_snapshot.go — снимок того, что bj нужно в базе DEPO (Fancy): объекты расширения FTV_M2M*
|
||||||
|
// (таблицы, процедуры, триггеры — с хешем текста), зависимости поставки Fancy, константы, вшитые в
|
||||||
|
// процедуры Фонтвьель (@WHOS, места, типы), задание SQL Agent bj2depo, права логина bj и счётчики
|
||||||
|
// трансферных таблиц по STATE. Данных клиентов в снимке нет. Нужен, чтобы сравнить установки (28 и 9)
|
||||||
|
// и поймать ручные правки в DEPO (22.10.2026: программист Фонтвьель вносит изменения в DEPO на 9).
|
||||||
|
// Снимки копятся в depoSnapshotDir; GET /admin/status/depo-snapshot — свежий снимок (JSON).
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const depoSnapshotDir = "/var/lib/bj/.bj/depo-snapshots"
|
||||||
|
|
||||||
|
// DepoSnapshot — состояние DEPO для bj.
|
||||||
|
type DepoSnapshot struct {
|
||||||
|
At time.Time `json:"at"`
|
||||||
|
Server string `json:"server"`
|
||||||
|
Database string `json:"database"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Collation string `json:"collation"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
Objects []DepoObject `json:"objects"` // FTV_M2M* и bj*
|
||||||
|
Deps map[string]bool `json:"deps"` // зависимости поставки Fancy: есть/нет
|
||||||
|
Consts map[string]string `json:"consts,omitempty"` // константы, вшитые в процедуры
|
||||||
|
Settings []map[string]any `json:"settings,omitempty"` // FTV_M2M_SETTINGS, если есть
|
||||||
|
Jobs []DepoJob `json:"jobs,omitempty"` // SQL Agent (msdb), если читается
|
||||||
|
Grants []string `json:"grants,omitempty"` // права на FTV_M2M*
|
||||||
|
States map[string]string `json:"states,omitempty"` // таблица → «STATE:кол-во …»
|
||||||
|
Errors []string `json:"errors,omitempty"` // что не удалось прочитать
|
||||||
|
}
|
||||||
|
|
||||||
|
// DepoObject — объект базы: тип, даты, для модулей — длина и SHA-256 текста, для таблиц — столбцы.
|
||||||
|
type DepoObject struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Modified string `json:"modified"`
|
||||||
|
Parent string `json:"parent,omitempty"` // для триггера — таблица
|
||||||
|
DefLen int `json:"def_len,omitempty"`
|
||||||
|
DefHash string `json:"def_hash,omitempty"`
|
||||||
|
Columns []string `json:"columns,omitempty"`
|
||||||
|
Rows int64 `json:"rows,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DepoJob — задание SQL Agent, связанное с bj.
|
||||||
|
type DepoJob struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Steps []string `json:"steps"`
|
||||||
|
Schedule string `json:"schedule,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var depoSnapDeps = []string{"D_CLISER_M2M", "D_CLISER_M2M_DOL", "OD_M2M_ACCS", "OD_ONLINE_M2M_DEPO", "OD_ONLINE_M2M_DEPO_II",
|
||||||
|
"FU_NEXT_DOC_NUM", "GEN_ID", "OD_PR_INS_WIRS", "OD_DOC_CATS", "OD_STEP_TYPES", "OD_SYS_TABS"}
|
||||||
|
|
||||||
|
// константы в процедурах Фонтвьель (README deploy/depo/fancy-m2m): «@WHOS int = 383118» и т. п.
|
||||||
|
var depoSnapConstRe = regexp.MustCompile(`(?i)@(WHOS|PLACE\w*|MARKET\w*|ACC\w*TYPE\w*|DOC\w*CAT\w*|CAT\w*)\s+int\s*=\s*(-?\d+)`)
|
||||||
|
|
||||||
|
// depoSnapshotTake — снять снимок (логином bj из настроек «Обмен Fancy»).
|
||||||
|
func depoSnapshotTake(ctx context.Context, fs FancyMSSQLSettings) (*DepoSnapshot, error) {
|
||||||
|
if strings.TrimSpace(fs.Server) == "" {
|
||||||
|
return nil, fmt.Errorf("подключение к DEPO не настроено")
|
||||||
|
}
|
||||||
|
db, err := fancyOpen(fs, fs.DepoDB())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 90*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
s := &DepoSnapshot{At: time.Now(), Server: fs.Server, Deps: map[string]bool{}, Consts: map[string]string{}, States: map[string]string{}}
|
||||||
|
fail := func(what string, err error) { s.Errors = append(s.Errors, what+": "+err.Error()) }
|
||||||
|
|
||||||
|
if err := db.QueryRowContext(ctx, `SELECT DB_NAME(), CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(64)), CAST(DATABASEPROPERTYEX(DB_NAME(),'Collation') AS nvarchar(128)), SUSER_SNAME()`).
|
||||||
|
Scan(&s.Database, &s.Version, &s.Collation, &s.Login); err != nil {
|
||||||
|
fail("сервер", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.QueryContext(ctx, `SELECT o.name, o.type_desc, CONVERT(varchar(19),o.create_date,120), CONVERT(varchar(19),o.modify_date,120),
|
||||||
|
ISNULL(OBJECT_NAME(o.parent_object_id),''), ISNULL(m.definition,'')
|
||||||
|
FROM sys.objects o LEFT JOIN sys.sql_modules m ON m.object_id=o.object_id
|
||||||
|
WHERE o.is_ms_shipped=0 AND (o.name LIKE 'FTV[_]M2M%' OR o.name LIKE 'bj[_]%' OR o.name LIKE 'BJ[_]%') AND o.type IN ('U','P','TR','V','FN','IF','TF')
|
||||||
|
ORDER BY o.type_desc, o.name`)
|
||||||
|
if err != nil {
|
||||||
|
fail("объекты", err)
|
||||||
|
} else {
|
||||||
|
for rows.Next() {
|
||||||
|
var o DepoObject
|
||||||
|
var def string
|
||||||
|
if rows.Scan(&o.Name, &o.Type, &o.Created, &o.Modified, &o.Parent, &def) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if def != "" {
|
||||||
|
h := sha256.Sum256([]byte(def))
|
||||||
|
o.DefLen, o.DefHash = len([]rune(def)), hex.EncodeToString(h[:8])
|
||||||
|
for _, m := range depoSnapConstRe.FindAllStringSubmatch(def, -1) {
|
||||||
|
s.Consts[o.Name+"."+strings.ToUpper(m[1])] = m[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Objects = append(s.Objects, o)
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
}
|
||||||
|
for i := range s.Objects {
|
||||||
|
o := &s.Objects[i]
|
||||||
|
if o.Type != "USER_TABLE" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cr, err := db.QueryContext(ctx, `SELECT c.name, t.name, c.max_length, c.is_nullable FROM sys.columns c JOIN sys.types t ON t.user_type_id=c.user_type_id
|
||||||
|
WHERE c.object_id=OBJECT_ID(@t) ORDER BY c.column_id`, sql.Named("t", "dbo."+o.Name))
|
||||||
|
if err == nil {
|
||||||
|
for cr.Next() {
|
||||||
|
var n, t string
|
||||||
|
var ml int
|
||||||
|
var null bool
|
||||||
|
if cr.Scan(&n, &t, &ml, &null) == nil {
|
||||||
|
col := fmt.Sprintf("%s %s(%d)", n, t, ml)
|
||||||
|
if !null {
|
||||||
|
col += " NOT NULL"
|
||||||
|
}
|
||||||
|
o.Columns = append(o.Columns, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cr.Close()
|
||||||
|
}
|
||||||
|
_ = db.QueryRowContext(ctx, `SELECT SUM(p.rows) FROM sys.partitions p WHERE p.object_id=OBJECT_ID(@t) AND p.index_id IN (0,1)`, sql.Named("t", "dbo."+o.Name)).Scan(&o.Rows)
|
||||||
|
hasState := false
|
||||||
|
for _, c := range o.Columns {
|
||||||
|
if strings.HasPrefix(c, "STATE ") {
|
||||||
|
hasState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasState {
|
||||||
|
sr, err := db.QueryContext(ctx, `SELECT ISNULL(CAST(STATE AS nvarchar(40)),'NULL'), COUNT(*) FROM dbo.[`+o.Name+`] GROUP BY STATE ORDER BY 1`)
|
||||||
|
if err == nil {
|
||||||
|
var parts []string
|
||||||
|
for sr.Next() {
|
||||||
|
var st string
|
||||||
|
var n int64
|
||||||
|
if sr.Scan(&st, &n) == nil {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s:%d", st, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sr.Close()
|
||||||
|
s.States[o.Name] = strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range depoSnapDeps {
|
||||||
|
var n int
|
||||||
|
_ = db.QueryRowContext(ctx, `SELECT COUNT(*) FROM sys.objects WHERE name=@n`, sql.Named("n", d)).Scan(&n)
|
||||||
|
s.Deps[d] = n > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if sr, err := db.QueryContext(ctx, `IF OBJECT_ID('dbo.FTV_M2M_SETTINGS') IS NOT NULL SELECT * FROM dbo.FTV_M2M_SETTINGS`); err == nil {
|
||||||
|
cols, _ := sr.Columns()
|
||||||
|
for sr.Next() {
|
||||||
|
vals := make([]sql.NullString, len(cols))
|
||||||
|
ptr := make([]any, len(cols))
|
||||||
|
for i := range vals {
|
||||||
|
ptr[i] = &vals[i]
|
||||||
|
}
|
||||||
|
if sr.Scan(ptr...) == nil {
|
||||||
|
row := map[string]any{}
|
||||||
|
for i, c := range cols {
|
||||||
|
if vals[i].Valid {
|
||||||
|
row[c] = vals[i].String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Settings = append(s.Settings, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// задания SQL Agent: шаги — из sysjobs/sysjobsteps; расписание — отдельно (sysschedules часто закрыт)
|
||||||
|
jr, err := db.QueryContext(ctx, `SELECT j.name, j.enabled, ISNULL(st.step_name,''), ISNULL(st.database_name,''), ISNULL(st.command,''), CAST(j.job_id AS nvarchar(40))
|
||||||
|
FROM msdb.dbo.sysjobs j LEFT JOIN msdb.dbo.sysjobsteps st ON st.job_id=j.job_id
|
||||||
|
WHERE j.name LIKE '%bj%' OR st.command LIKE '%FTV[_]M2M%' ORDER BY j.name, st.step_id`)
|
||||||
|
if err != nil {
|
||||||
|
fail("задания SQL Agent (msdb)", err)
|
||||||
|
} else {
|
||||||
|
byName := map[string]*DepoJob{}
|
||||||
|
ids := map[string]string{}
|
||||||
|
var order []string
|
||||||
|
for jr.Next() {
|
||||||
|
var name, step, dbn, cmd, id string
|
||||||
|
var en bool
|
||||||
|
if jr.Scan(&name, &en, &step, &dbn, &cmd, &id) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
j := byName[name]
|
||||||
|
if j == nil {
|
||||||
|
j = &DepoJob{Name: name, Enabled: en}
|
||||||
|
byName[name], ids[name] = j, id
|
||||||
|
order = append(order, name)
|
||||||
|
}
|
||||||
|
if step != "" {
|
||||||
|
j.Steps = append(j.Steps, fmt.Sprintf("%s [%s]: %s", step, dbn, strings.Join(strings.Fields(cmd), " ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jr.Close()
|
||||||
|
for _, n := range order {
|
||||||
|
var sched string
|
||||||
|
if err := db.QueryRowContext(ctx, `SELECT TOP 1 CONCAT('freq_type=',sc.freq_type,' subday=',sc.freq_subday_type,'/',sc.freq_subday_interval,' enabled=',sc.enabled)
|
||||||
|
FROM msdb.dbo.sysjobschedules js JOIN msdb.dbo.sysschedules sc ON sc.schedule_id=js.schedule_id WHERE CAST(js.job_id AS nvarchar(40))=@id`, sql.Named("id", ids[n])).Scan(&sched); err == nil {
|
||||||
|
byName[n].Schedule = sched
|
||||||
|
}
|
||||||
|
s.Jobs = append(s.Jobs, *byName[n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
noDef := 0
|
||||||
|
for _, o := range s.Objects {
|
||||||
|
if o.Type != "USER_TABLE" && o.DefHash == "" {
|
||||||
|
noDef++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if noDef > 0 {
|
||||||
|
s.Errors = append(s.Errors, fmt.Sprintf("тексты %d процедур недоступны логину bj (нет VIEW DEFINITION) — изменения видны по дате modified", noDef))
|
||||||
|
}
|
||||||
|
|
||||||
|
gr, err := db.QueryContext(ctx, `SELECT pr.name, p.permission_name, p.state_desc, ISNULL(OBJECT_NAME(p.major_id),'(база)')
|
||||||
|
FROM sys.database_permissions p JOIN sys.database_principals pr ON pr.principal_id=p.grantee_principal_id
|
||||||
|
WHERE p.major_id=0 OR OBJECT_NAME(p.major_id) LIKE 'FTV[_]M2M%' ORDER BY 1,4,2`)
|
||||||
|
if err != nil {
|
||||||
|
fail("права", err)
|
||||||
|
} else {
|
||||||
|
for gr.Next() {
|
||||||
|
var who, perm, st, obj string
|
||||||
|
if gr.Scan(&who, &perm, &st, &obj) == nil && (obj != "(база)" || strings.Contains(strings.ToLower(who), "bj")) {
|
||||||
|
s.Grants = append(s.Grants, fmt.Sprintf("%s: %s %s на %s", who, st, perm, obj))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gr.Close()
|
||||||
|
}
|
||||||
|
if rr, err := db.QueryContext(ctx, `SELECT r.name FROM sys.database_role_members m JOIN sys.database_principals r ON r.principal_id=m.role_principal_id
|
||||||
|
JOIN sys.database_principals u ON u.principal_id=m.member_principal_id WHERE u.name=USER_NAME()`); err == nil {
|
||||||
|
for rr.Next() {
|
||||||
|
var r string
|
||||||
|
if rr.Scan(&r) == nil {
|
||||||
|
s.Grants = append(s.Grants, "роль логина bj: "+r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rr.Close()
|
||||||
|
}
|
||||||
|
sort.Strings(s.Grants)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// depoSnapshotSave — записать снимок в историю (depo-snapshots/ГГГГММДД-ЧЧММСС.json); путь к файлу.
|
||||||
|
func depoSnapshotSave(s *DepoSnapshot) (string, error) {
|
||||||
|
if err := os.MkdirAll(depoSnapshotDir, 0o750); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b, err := json.MarshalIndent(s, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
p := filepath.Join(depoSnapshotDir, s.At.Format("20060102-150405")+".json")
|
||||||
|
return p, os.WriteFile(p, b, 0o640)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerDepoSnapshot — GET /admin/status/depo-snapshot: снять, сохранить и отдать снимок (JSON).
|
||||||
|
// Плюс снимок раз в сутки в фоне (первый — через 3 минуты после запуска): история изменений DEPO.
|
||||||
|
func registerDepoSnapshot(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||||
|
mux.HandleFunc("/admin/status/depo-snapshot", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s, err := depoSnapshotTake(r.Context(), rc.Snapshot().Fancy)
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = depoSnapshotSave(s)
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
_ = enc.Encode(s)
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
time.Sleep(3 * time.Minute)
|
||||||
|
for {
|
||||||
|
if s, err := depoSnapshotTake(context.Background(), rc.Snapshot().Fancy); err == nil {
|
||||||
|
_, _ = depoSnapshotSave(s)
|
||||||
|
}
|
||||||
|
time.Sleep(24 * time.Hour)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -215,17 +215,18 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
|||||||
registerOutgoing(mux, adminTpl, outDeps)
|
registerOutgoing(mux, adminTpl, outDeps)
|
||||||
registerManual(mux, rc, svc) // ручной ввод исходящих переводов по бумажным поручениям
|
registerManual(mux, rc, svc) // ручной ввод исходящих переводов по бумажным поручениям
|
||||||
registerJournal(mux, adminTpl, svc, incomingStore)
|
registerJournal(mux, adminTpl, svc, incomingStore)
|
||||||
registerNSDHealth(mux, rc) // связь с НРД: шапка (json) и «Проверить связь сейчас»
|
registerNSDHealth(mux, rc) // связь с НРД: шапка (json) и «Проверить связь сейчас»
|
||||||
registerBackup(mux, rc) // резервное копирование и восстановление (backup.go)
|
registerBackup(mux, rc) // резервное копирование и восстановление (backup.go)
|
||||||
registerIgate(mux, rc) // интеграционный шлюз НРД: состояние и установка (igate.go)
|
registerIgate(mux, rc) // интеграционный шлюз НРД: состояние и установка (igate.go)
|
||||||
registerReadiness(mux, rc) // итоговая проверка готовности (мастер, шаг 10)
|
registerReadiness(mux, rc) // итоговая проверка готовности (мастер, шаг 10)
|
||||||
registerQuikReady(mux, rc) // проверка модуля неторговых поручений QUIK (только чтение)
|
registerDepoSnapshot(mux, rc) // снимок объектов bj в DEPO (история изменений)
|
||||||
registerHTTPS(mux, rc) // HTTPS веб-интерфейса: перенаправление, свой сертификат
|
registerQuikReady(mux, rc) // проверка модуля неторговых поручений QUIK (только чтение)
|
||||||
registerTime(mux, rc) // сервер времени (NTP)
|
registerHTTPS(mux, rc) // HTTPS веб-интерфейса: перенаправление, свой сертификат
|
||||||
registerTimeZone(mux) // часовой пояс сервера
|
registerTime(mux, rc) // сервер времени (NTP)
|
||||||
registerHostname(mux, rc) // доменные имена сервера (сертификат HTTPS)
|
registerTimeZone(mux) // часовой пояс сервера
|
||||||
registerLetters(mux, rc) // готовые письма: НРД, ARQA, администратор DEPO
|
registerHostname(mux, rc) // доменные имена сервера (сертификат HTTPS)
|
||||||
registerLKAPI(mux, rc, svc) // API для модуля личного кабинета клиента (bj-lk)
|
registerLetters(mux, rc) // готовые письма: НРД, ARQA, администратор DEPO
|
||||||
|
registerLKAPI(mux, rc, svc) // API для модуля личного кабинета клиента (bj-lk)
|
||||||
registerHealth(mux)
|
registerHealth(mux)
|
||||||
registerQuikOps(mux, rc, svc)
|
registerQuikOps(mux, rc, svc)
|
||||||
registerNPR(mux, adminTpl, rc)
|
registerNPR(mux, adminTpl, rc)
|
||||||
|
|||||||
@@ -932,6 +932,15 @@
|
|||||||
<label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="redirect" style="width:auto"{{if .Settings.HTTPS.RedirectOn}} checked{{end}}> <b>Открывать bj только по HTTPS</b> — браузер с http:// перенаправляется на https://</label>
|
<label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="redirect" style="width:auto"{{if .Settings.HTTPS.RedirectOn}} checked{{end}}> <b>Открывать bj только по HTTPS</b> — браузер с http:// перенаправляется на https://</label>
|
||||||
<button type="submit" class="btn" style="margin-top:8px">Сохранить</button>
|
<button type="submit" class="btn" style="margin-top:8px">Сохранить</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="post" action="/admin/setup/hostname" style="margin:12px 0 8px">
|
||||||
|
<input type="hidden" name="section" value="sec-https">
|
||||||
|
<label for="set-hostnames" style="display:block;margin-bottom:4px">Доменное имя сервера bj</label>
|
||||||
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
|
<input id="set-hostnames" name="hostnames" value="{{range $i, $n := .Settings.HTTPS.Hostnames}}{{if $i}} {{end}}{{$n}}{{end}}" placeholder="например, m2m.corp.example.ru (несколько — через пробел)" style="min-width:320px;flex:1">
|
||||||
|
<button type="submit" class="btn">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
<p class="muted" style="font-size:12.5px;margin:4px 0 0">bj впишет имя в свой сертификат — интерфейс будет открываться по нему без предупреждения о несовпадении имени. DNS-запись заводит администратор сети.</p>
|
||||||
|
</form>
|
||||||
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent)">Сертификат организации</summary>
|
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent)">Сертификат организации</summary>
|
||||||
<form method="post" action="/admin/setup/https/cert" enctype="multipart/form-data" style="display:grid;gap:8px;max-width:520px;margin-top:8px">
|
<form method="post" action="/admin/setup/https/cert" enctype="multipart/form-data" style="display:grid;gap:8px;max-width:520px;margin-top:8px">
|
||||||
<label>Сертификат (PEM, с цепочкой)<input type="file" name="cert" accept=".pem,.crt,.cer" style="width:100%"></label>
|
<label>Сертификат (PEM, с цепочкой)<input type="file" name="cert" accept=".pem,.crt,.cer" style="width:100%"></label>
|
||||||
|
|||||||
@@ -155,6 +155,12 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>{{end}}
|
</div>{{end}}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>DEPO: что нужно bj</h2>
|
||||||
|
<p class="muted">Снимок объектов bj в базе DEPO: трансферные таблицы и процедуры FTV_M2M* (даты изменения), зависимости поставки Fancy, права логина bj, счётчики по статусам. Данных клиентов в снимке нет. bj снимает его раз в сутки и хранит историю — по ней видно, что и когда меняли в DEPO.</p>
|
||||||
|
<a class="btn" href="/admin/status/depo-snapshot" target="_blank" rel="noopener">Снять снимок сейчас (JSON)</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{if devOn}}{{/* дорожная карта разработки — только в режиме разработки */}}
|
{{if devOn}}{{/* дорожная карта разработки — только в режиме разработки */}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Что подключается на следующих этапах</h2>
|
<h2>Что подключается на следующих этапах</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user