diff --git a/internal/lkgateway/onboarding_test.go b/internal/lkgateway/onboarding_test.go index 71f1ccf..d88e89e 100644 --- a/internal/lkgateway/onboarding_test.go +++ b/internal/lkgateway/onboarding_test.go @@ -494,4 +494,23 @@ func TestDepoScriptView(t *testing.T) { if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "8 знаков") { t.Fatalf("короткий пароль: %d %s", w.Code, w.Body.String()) } + // экран пересобирает скрипт на ходу — и до того, как вписан адрес сервера; без адреса ничего не сохраняется + form.Set("bj_password", "Other123x") + form.Set("server", "") + if w := post(form); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "N'Other123x';") { + t.Fatalf("скрипт без адреса сервера: %d %s", w.Code, w.Body.String()) + } + if f := rc.Snapshot().Fancy; f.Server != "10.0.0.5" || f.Password != "Secret123x" { + t.Fatalf("без адреса сервера настройки не должны меняться: %q", f.Server) + } + // PostgreSQL — свой скрипт; без имени базы — понятная причина вместо скрипта + form.Set("db_type", "postgres") + form.Set("depo_database", "") + if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя базы") { + t.Fatalf("PostgreSQL без базы: %d %s", w.Code, w.Body.String()) + } + form.Set("depo_database", "depo") + if w := post(form); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "PostgreSQL") { + t.Fatalf("PostgreSQL: %d %s", w.Code, w.Body.String()) + } } diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index 3a364a5..ad49dfe 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -52,6 +52,37 @@ .wiz-row-l { color:var(--muted); font-weight:400; font-size:14.5px; min-width:170px; margin:0; } .wiz-opt { display:flex; gap:8px; align-items:flex-start; font-weight:400; margin:0 0 6px; } .wiz-opt .muted { font-size:13px; } +/* Учёт DEPO: вопросы по порядку (номер — счётчиком), выбор — плитками, под ними — панель выбранного способа */ +.dq { counter-reset:dq; } +.dq-s { padding:18px 0 20px; border-top:1px solid var(--border); } +.dq-s:first-child { border-top:0; padding-top:2px; } +.dq-h { display:flex; align-items:center; gap:10px; font-size:15.5px; font-weight:650; margin:0 0 12px; } +.dq-h::before { counter-increment:dq; content:counter(dq); flex:none; width:22px; height:22px; border-radius:50%; background:var(--accent-weak); color:var(--accent); display:grid; place-items:center; font-size:12px; font-weight:700; } +.dq-tiles { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; } +.dq-tile { display:flex; gap:10px; align-items:flex-start; padding:12px 14px; border:1px solid var(--border); border-radius:12px; cursor:pointer; font-weight:400; margin:0; transition:border-color .15s var(--ease), background .15s var(--ease); } +.dq-tile:hover { border-color:var(--border-strong); } +.dq-tile:has(input:checked) { border-color:var(--accent); background:var(--accent-weak); } +.dq-tile b { display:block; font-weight:600; font-size:14.5px; } +.dq-tile small { display:block; color:var(--muted); font-size:13px; line-height:1.4; margin-top:3px; } +.dq-alt { display:inline-flex; gap:8px; align-items:flex-start; margin:12px 0 0; font-weight:400; font-size:13.5px; color:var(--text-2); cursor:pointer; } +.dq-grid { display:grid; gap:10px 12px; } +.dq-grid label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); } +.dq-grid input, .dq-grid select { display:block; width:100%; box-sizing:border-box; margin-top:5px; } +.dq-srv { grid-template-columns:minmax(0,1.5fr) minmax(0,.55fr) minmax(0,1.25fr); } +.dq-2 { grid-template-columns:repeat(2,minmax(0,1fr)); } +.dq-opt { color:var(--muted); font-weight:400; } +.dq-note { color:var(--muted); font-size:13px; margin:8px 0 0; } +.dq-panel { margin-top:18px; } +.wiz-card .dq-go { margin-top:16px; border-radius:999px; padding:10px 22px; font-size:14.5px; font-weight:600; } +.wiz-card .dq-mono { font-family:var(--mono); } +.dq-sql { margin-top:14px; } +.dq-sql-head { display:flex; align-items:center; gap:8px; margin:0 0 6px; justify-content:flex-start; } +.dq-sql-st { flex:1; font-size:12.5px; color:var(--muted); } +.dq-sql-st.bad { color:var(--warn); } +.dq-sql pre { max-height:380px; min-height:140px; font-size:12px; margin:0; transition:opacity .2s; } +.dq-sql pre.dq-busy { opacity:.5; } +.dq.is-pg [data-ms], .dq:not(.is-pg) [data-pg] { display:none !important; } +@media (max-width:600px) { .dq-tiles, .dq-srv, .dq-2 { grid-template-columns:minmax(0,1fr); } .dq-sql-head { flex-wrap:wrap; } .dq-sql-st { flex-basis:100%; } } .tooltip { display:inline-block; background:var(--border); color:var(--muted); border-radius:50%; width:16px; height:16px; line-height:16px; text-align:center; font-size:11px; cursor:help; margin-left:4px; } .help-block { background:var(--accent-weak); padding:10px 14px; margin:10px 0; font-size:13px; border-radius:10px; } .help-block strong { color:var(--accent); } @@ -132,70 +163,113 @@ {{else}} {{$sub := .Sub}} {{if eq $sub "setup"}} -

bj сверяет каждый перевод с учётом DEPO и сам создаёт в нём поручения. Укажите сервер DEPO; имя базы можно не писать — bj найдёт её по учётным таблицам.

- {{$mode := .Settings.Fancy.DepoJobMode}} -
-
- - - -
- {{if .DepoCands}} - {{else}}{{end}} +

bj сверяет каждый перевод с учётом DEPO и сам создаёт в нём поручения. Четыре вопроса по порядку — из ответов bj соберёт подготовку базы.

+ {{$job := .Settings.Fancy.DepoJobMode}}{{$pg := eq .Settings.Fancy.DBType "postgres"}} + +
+

Какая СУБД у DEPO

+
+ + +
+
-
Как подготовить базу
- -
- - +
+

Где база DEPO

+
+ + + {{if .DepoCands}} + {{else}}{{end}}
- -
+ +
+

Где запускать обработку обмена

+
+ + +
+

Для PostgreSQL обработку запускает сервер bj — выбирать не нужно.

+
+ +
+

Как подготовить базу

+
+ + +
+ + +
+
+ +
-

Скрипт создаст этот логин на сервере DEPO — bj будет входить с ним. Пароль bj придумал сам, можно вписать свой. База DEPO и пароль стоят в начале скрипта — администратор может их поправить.

-
- - +

bj создаст свой логин, таблицы обмена и задание обработки, выдаст права и проверит подключение.

+ +
+ +
+
+ +
- -

Администратор выполняет скрипт в SSMS под логином с правами sysadmin — потом нажмите «Проверить подключение».

+

Скрипт создаст этот логин — с ним bj будет входить в базу DEPO. Пароль придумал bj, можно вписать свой.

+
+
+ Скрипт собирается из ответов выше и обновляется сам. + + +
+

+        
+

Администратор выполняет скрипт в SSMS или sqlcmd под логином с правами sysadmin.Администратор выполняет скрипт в psql суперпользователем (postgres). Когда выполнит — нажмите «Проверить подключение».

{{template "letterCard" (letter "dba")}} +
- - -
Кто запускает обработку обмена
- - -
-
+
- + .then(function(x){ if(my!==seq) return; pre.classList.remove('dq-busy'); + if(x.ok){ pre.textContent=x.t; st.textContent='Скрипт собирается из ответов выше и обновляется сам.'; st.classList.remove('bad'); } + else { st.textContent=x.t.trim().replace(/^./,function(c){return c.toUpperCase()})+' — скрипт обновится, когда поправите.'; st.classList.add('bad'); } }) + .catch(function(){ if(my!==seq) return; last=''; pre.classList.remove('dq-busy'); st.textContent='Не получилось собрать скрипт — обновите страницу.'; st.classList.add('bad'); }); + } + function changed(){ sync(); clearTimeout(timer); timer=setTimeout(load, 350); } + f.addEventListener('input', changed); f.addEventListener('change', changed); + document.getElementById('wiz-depo-copy').onclick=function(){ var b=this; + window.bjCopy(pre.textContent).then(function(ok){ b.textContent=ok?'Скопировано ✓':'Выделите и скопируйте вручную'; b.classList.toggle('done',!!ok); + setTimeout(function(){ b.textContent='Скопировать'; b.classList.remove('done'); }, 2000); }); }; + sync(); load(); + })(); {{else if eq $sub "done"}} diff --git a/internal/lkgateway/wizard_depo.go b/internal/lkgateway/wizard_depo.go index 696861d..138ed2e 100644 --- a/internal/lkgateway/wizard_depo.go +++ b/internal/lkgateway/wizard_depo.go @@ -236,7 +236,8 @@ func (h *setupHandlers) wizardDepoRecheck(w http.ResponseWriter, r *http.Request wizDepoGo(w, r, "done", "всё перепроверено", true) } -// depoSetupForm — сервер DEPO и способ обработки из формы экрана «Подготовка базы» (логин bj не трогается). +// depoSetupForm — сервер DEPO и способ обработки из формы экрана «Подготовка базы» (логин bj не трогается; +// пустой адрес сервера не ошибка — скрипту он не нужен, подключению проверяют вызывающие). func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, string, bool, string) { s := h.rc.Snapshot().Fancy dbType := "mssql" @@ -271,9 +272,6 @@ func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, stri if dbType == "postgres" && db == "" { return s, "", agent, "для PostgreSQL укажите имя базы DEPO" } - if server == "" { - return s, db, agent, "укажите адрес сервера DEPO" - } return s, db, agent, "" } @@ -307,7 +305,7 @@ func depoScriptCreds(r *http.Request, s *FancyMSSQLSettings) string { } // wizardDepoScript — POST /admin/setup/wizard-depo/script.sql: один скрипт подготовки базы файлом -// (view=1 — текстом для показа на странице). +// (view=1 — текстом для показа на странице: экран пересобирает его при каждой правке ответов). func (h *setupHandlers) wizardDepoScript(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || !requireDepo(w, r) { return @@ -330,9 +328,11 @@ func (h *setupHandlers) wizardDepoScript(w http.ResponseWriter, r *http.Request) } depoEnsureLogin(&s) s.DepoTablesMode = "script" - if err := h.rc.UpdateFancy(s); err != nil { - fail("не сохранилось: " + err.Error()) - return + if s.Server != "" { // скрипт собирается и до адреса сервера (экран обновляет его на ходу) — без адреса не сохраняем + if err := h.rc.UpdateFancy(s); err != nil { + fail("не сохранилось: " + err.Error()) + return + } } body := depoSetupScript(s, db, s.User, s.Password, agent) w.Header().Set("Cache-Control", "no-store") @@ -356,6 +356,9 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request) return } s, db, agent, bad := h.depoSetupForm(r) + if bad == "" && s.Server == "" { + bad = "укажите адрес сервера DEPO" + } if bad != "" { wizDepoGo(w, r, "setup", bad, false) return @@ -417,7 +420,7 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request) found, err := depoDiscover(ctx, s) switch { case err != nil || len(found) == 0: - wizDepoGo(w, r, "setup", "логину "+s.User+" не видна ни одна база DEPO — выполнен ли скрипт? Можно указать базу вручную («Дополнительно»)", false) + wizDepoGo(w, r, "setup", "логину "+s.User+" не видна ни одна база DEPO — выполнен ли скрипт? Можно вписать имя базы в поле «Имя базы»", false) return case len(found) > 1: http.Redirect(w, r, "/admin/wizard?step=3&sub=setup&cands="+url.QueryEscape(strings.Join(found, ","))+