Мастер настройки: после создания администратора — сразу в мастер; 7 шагов (лицензия, база bj, учёт DEPO, Валидата, сертификаты, шлюз НРД, проверка) с пояснениями что/зачем/где взять, каждый можно пропустить; исправлено имя поля URL ИШ

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-09-24 14:23:12 +03:00
parent 2350fdf5aa
commit ea87958f43
3 changed files with 103 additions and 38 deletions
+4 -4
View File
@@ -101,17 +101,17 @@ func registerFirstRun(mux *http.ServeMux) {
acancel()
if err != nil {
log.Printf("auth: первичная настройка: ключ продукта не принят: %v", err)
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Лицензия: ключ продукта не принят — "+err.Error()+". Введите его в разделе «Лицензия».")+"#sec-license", http.StatusSeeOther)
http.Redirect(w, r, "/admin/wizard?step=1&flash="+url.QueryEscape("Администратор создан. Ключ продукта не принят: "+err.Error()+". Проверьте его и введите ещё раз."), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Лицензия активирована. Дальше — подключение к базе DEPO, затем реквизиты НРД (кнопка «Определить из DEPO»)."), http.StatusSeeOther)
http.Redirect(w, r, "/admin/wizard?flash="+url.QueryEscape("Администратор создан, лицензия активирована. Мастер проведёт по остальным шагам — любой можно пропустить."), http.StatusSeeOther)
return
}
if d.NeedKey {
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Администратор создан. Лицензия ещё не активирована — введите ключ продукта в разделе «Лицензия», иначе модули и обновления недоступны.")+"#sec-license", http.StatusSeeOther)
http.Redirect(w, r, "/admin/wizard?step=1&flash="+url.QueryEscape("Администратор создан. Начнём с лицензии: без ключа продукта модули и обновления недоступны (шаг можно пропустить)."), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/setup", http.StatusSeeOther)
http.Redirect(w, r, "/admin/wizard", http.StatusSeeOther)
return
}
w.WriteHeader(http.StatusUnauthorized)
+20 -6
View File
@@ -166,9 +166,14 @@ type WizardData struct {
Step int
Settings Settings
Suggest *DepoRequisites // предложение реквизитов из DEPO (после «Определить из DEPO»)
Lic LicenseStatus
DepoLicensed bool
DepoDB string
Certs []cryptocli.Certificate
Flash string
Done struct {
License bool
Depo bool
Postgres bool
Crypto bool
Certs bool
@@ -193,6 +198,11 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
Certs: h.listCertsForUI(),
Flash: r.URL.Query().Get("flash"),
}
d.Lic = licenseStatus(h.rc)
d.Done.License = d.Lic.Valid
d.DepoLicensed = moduleAllowed(h.rc, "quik-mnp")
d.DepoDB = s.Fancy.DepoDB()
d.Done.Depo = strings.TrimSpace(s.Fancy.Server) != "" || !d.DepoLicensed
d.Done.Postgres = s.Postgres.DSN != ""
d.Done.Crypto = s.Crypto.Provider != "" && s.Crypto.Provider != "stub" && s.Crypto.ModulePath != ""
d.Done.Certs = len(d.Certs) > 0
@@ -202,22 +212,26 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
// Определяем текущий шаг.
step := 1
if v := r.URL.Query().Get("step"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 5 {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 7 {
step = n
}
} else {
// Авто: первый незавершённый.
switch {
case !d.Done.Postgres:
case !d.Done.License:
step = 1
case !d.Done.Crypto:
case !d.Done.Postgres:
step = 2
case !d.Done.Certs:
case !d.Done.Depo:
step = 3
case !d.Done.NSD:
case !d.Done.Crypto:
step = 4
default:
case !d.Done.Certs:
step = 5
case !d.Done.NSD:
step = 6
default:
step = 7
}
}
d.Step = step
@@ -26,21 +26,72 @@
<p>Пошаговый мастер первого запуска — состояние сохраняется после каждого шага</p>
</div>
<div class="wizard-progress">
<div class="wstep {{if .Done.Postgres}}done{{end}} {{if eq .Step 1}}current{{end}}"><span class="bub">{{if .Done.Postgres}}✓{{else}}1{{end}}</span><span class="lbl">База</span></div>
<div class="wstep {{if .Done.Crypto}}done{{end}} {{if eq .Step 2}}current{{end}}"><span class="bub">{{if .Done.Crypto}}✓{{else}}2{{end}}</span><span class="lbl">Валидата</span></div>
<div class="wstep {{if .Done.Certs}}done{{end}} {{if eq .Step 3}}current{{end}}"><span class="bub">{{if .Done.Certs}}✓{{else}}3{{end}}</span><span class="lbl">Сертификаты</span></div>
<div class="wstep {{if .Done.NSD}}done{{end}} {{if eq .Step 4}}current{{end}}"><span class="bub">{{if .Done.NSD}}✓{{else}}4{{end}}</span><span class="lbl">Шлюз НРД</span></div>
<div class="wstep {{if .Done.TestRun}}done{{end}} {{if eq .Step 5}}current{{end}}"><span class="bub">{{if .Done.TestRun}}✓{{else}}5{{end}}</span><span class="lbl">Проверка</span></div>
<div class="wizard-progress" style="max-width:860px">
<div class="wstep {{if .Done.License}}done{{end}} {{if eq .Step 1}}current{{end}}"><span class="bub">{{if .Done.License}}✓{{else}}1{{end}}</span><span class="lbl">Лицензия</span></div>
<div class="wstep {{if .Done.Postgres}}done{{end}} {{if eq .Step 2}}current{{end}}"><span class="bub">{{if .Done.Postgres}}✓{{else}}2{{end}}</span><span class="lbl">База bj</span></div>
<div class="wstep {{if .Done.Depo}}done{{end}} {{if eq .Step 3}}current{{end}}"><span class="bub">{{if .Done.Depo}}✓{{else}}3{{end}}</span><span class="lbl">Учёт DEPO</span></div>
<div class="wstep {{if .Done.Crypto}}done{{end}} {{if eq .Step 4}}current{{end}}"><span class="bub">{{if .Done.Crypto}}✓{{else}}4{{end}}</span><span class="lbl">Валидата</span></div>
<div class="wstep {{if .Done.Certs}}done{{end}} {{if eq .Step 5}}current{{end}}"><span class="bub">{{if .Done.Certs}}✓{{else}}5{{end}}</span><span class="lbl">Сертификаты</span></div>
<div class="wstep {{if .Done.NSD}}done{{end}} {{if eq .Step 6}}current{{end}}"><span class="bub">{{if .Done.NSD}}✓{{else}}6{{end}}</span><span class="lbl">Шлюз НРД</span></div>
<div class="wstep {{if .Done.TestRun}}done{{end}} {{if eq .Step 7}}current{{end}}"><span class="bub">{{if .Done.TestRun}}✓{{else}}7{{end}}</span><span class="lbl">Проверка</span></div>
</div>
<p style="text-align:center;margin:-14px 0 18px;font-size:13px"><a href="/admin/">Пропустить мастер и перейти в систему →</a> <span class="muted">— вернуться к нему можно из «Настройка → Мастер настройки»</span></p>
{{if .Flash}}<div class="card wiz-card" style="background:var(--accent-weak)"><p style="margin:0">{{.Flash}}</p></div>{{end}}
<div class="wiz-card">
{{/* ============= ШАГ 1: PostgreSQL ============= */}}
{{/* ============= ШАГ 1: Лицензия ============= */}}
{{if eq .Step 1}}
<div class="card">
<h2><span class="dot {{if .Done.Postgres}}ok{{else}}err{{end}}"></span>Шаг 1. PostgreSQL</h2>
<h2><span class="dot {{if .Done.License}}ok{{else}}err{{end}}"></span>Шаг 1. Лицензия</h2>
<div class="help-block"><strong>Что это.</strong> Ключ продукта (BJ-XXXX-XXXX-XXXX) привязывает этот сервер к вашей лицензии.<br>
<strong>Зачем.</strong> От лицензии зависят модули (обмен с DEPO и QUIK, личный кабинет, НПР/УДС) и получение обновлений: bj скачает и поставит модули, входящие в лицензию, автоматически.<br>
<strong>Где взять.</strong> Портал клиента <a href="https://lic.zetit.ru/portal/" target="_blank">lic.zetit.ru/portal</a> или письмо от ZETIT.</div>
{{if .Done.License}}<p>✓ Лицензия активна: <strong>{{.Lic.Tenant}}</strong>, план {{.Lic.Plan}}, модули: {{if .Lic.Features}}{{range $i, $f := .Lic.Features}}{{if $i}}, {{end}}{{$f}}{{end}}{{else}}только ядро{{end}}; {{.Lic.Message}}</p>{{end}}
<form method="post" action="/admin/setup/license/activate" style="margin-top:12px;display:grid;gap:10px">
<input type="hidden" name="return_to" value="/admin/wizard?step=1">
<label>Ключ продукта</label>
<input type="text" name="code" placeholder="BJ-XXXX-XXXX-XXXX" autocomplete="off" style="padding:8px;text-transform:uppercase;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
<button type="submit" class="btn" style="justify-self:start">{{if .Done.License}}Сменить ключ{{else}}Активировать{{end}}</button>
</form>
<div style="margin-top:20px;display:flex;justify-content:space-between"><span></span>
{{if .Done.License}}<a href="/admin/wizard?step=2" class="btn" style="text-decoration:none">К шагу 2 →</a>{{else}}<a href="/admin/wizard?step=2" class="btn" style="background:var(--card);text-decoration:none">Пропустить (ввести позже) →</a>{{end}}</div>
</div>
{{end}}
{{/* ============= ШАГ 3: Учёт DEPO (Fancy) ============= */}}
{{if eq .Step 3}}
<div class="card">
<h2><span class="dot {{if .Done.Depo}}ok{{else}}err{{end}}"></span>Шаг 3. Учётная система DEPO (Fancy)</h2>
<div class="help-block"><strong>Что это.</strong> Подключение к базе депозитарного учёта DEPO (MS SQL / PostgreSQL).<br>
<strong>Зачем.</strong> bj сверяет с DEPO каждый перевод (клиент, счёт, остаток, квалификация) и передаёт в DEPO поручения и их исполнение — документы создаются в учёте автоматически. Отсюда же bj возьмёт ваши реквизиты в НРД (шаг 6), чтобы не вводить их вручную.<br>
<strong>Что нужно.</strong> Адрес сервера БД и логин с правами чтения DEPO и записи в трансферные таблицы FTV_M2M (выдаёт администратор СУБД).</div>
{{if not .DepoLicensed}}
<p class="muted">Модуль обмена с DEPO и QUIK не входит в текущую лицензию — шаг пропускается. После добавления модуля в лицензию он станет доступен здесь и в «Настройка → Обмен Fancy».</p>
{{else}}
{{if .Done.Depo}}<p>✓ Подключение сохранено: {{.Settings.Fancy.Server}}, база {{.DepoDB}}. {{if .Settings.Fancy.DepoTablesCheckOK}}Трансферные таблицы на месте.{{else}}Трансферные таблицы ещё не проверены — «Настройка → Обмен Fancy → Трансферные таблицы DEPO».{{end}}</p>{{end}}
<form method="post" action="/admin/setup/fancy" style="margin-top:12px;display:grid;gap:10px">
<input type="hidden" name="return_to" value="/admin/wizard?step=3">
<div><label>Тип СУБД</label><select name="db_type" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"><option value="mssql" {{if ne .Settings.Fancy.DBType "postgres"}}selected{{end}}>MS SQL Server</option><option value="postgres" {{if eq .Settings.Fancy.DBType "postgres"}}selected{{end}}>PostgreSQL</option></select></div>
<div><label>Сервер (адрес или host\instance)</label><input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="10.0.0.21" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"></div>
<div><label>Порт</label><input type="text" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{else}}1433{{end}}" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"></div>
<div><label>Логин БД</label><input type="text" name="user" value="{{.Settings.Fancy.User}}" autocomplete="off" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"></div>
<div><label>Пароль {{if .Settings.Fancy.Server}}<span class="muted">(пусто — оставить прежний)</span>{{end}}</label><input type="password" name="password" autocomplete="new-password" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"></div>
<input type="hidden" name="encrypt" value="{{if .Settings.Fancy.Encrypt}}{{.Settings.Fancy.Encrypt}}{{else}}disable{{end}}">
<div style="display:flex;gap:10px;flex-wrap:wrap"><button type="submit" class="btn">Сохранить</button>
{{if .Settings.Fancy.Server}}<button type="submit" class="btn" formaction="/admin/setup/fancy/test" style="background:var(--card)">Проверить соединение</button>{{end}}</div>
</form>
{{end}}
<div style="margin-top:20px;display:flex;justify-content:space-between">
<a href="/admin/wizard?step=2" class="btn" style="background:var(--card);text-decoration:none">← К шагу 2</a>
<a href="/admin/wizard?step=4" class="btn" style="{{if not .Done.Depo}}background:var(--card);{{end}}text-decoration:none">{{if .Done.Depo}}К шагу 4 →{{else}}Пропустить →{{end}}</a></div>
</div>
{{end}}
{{/* ============= ШАГ 1: PostgreSQL ============= */}}
{{if eq .Step 2}}
<div class="card">
<h2><span class="dot {{if .Done.Postgres}}ok{{else}}err{{end}}"></span>Шаг 2. PostgreSQL</h2>
<p>Сюда система пишет журнал сделок и принимает данные от команды Fansy.</p>
<div class="help-block">
@@ -70,16 +121,16 @@
<div style="margin-top:20px;display:flex;justify-content:space-between">
<span></span>
{{if .Done.Postgres}}<a href="/admin/wizard?step=2" class="btn" style="text-decoration:none">К шагу 2 →</a>{{else}}<button class="btn" disabled style="opacity:0.5;cursor:not-allowed">К шагу 2 → (сначала настройте PostgreSQL или нажмите «in-memory режим»)</button>{{end}}
{{if .Done.Postgres}}<a href="/admin/wizard?step=4" class="btn" style="text-decoration:none">К шагу 2 →</a>{{else}}<button class="btn" disabled style="opacity:0.5;cursor:not-allowed">К шагу 2 → (сначала настройте PostgreSQL или нажмите «in-memory режим»)</button>{{end}}
</div>
{{if not .Done.Postgres}}<p style="margin-top:8px"><a href="/admin/wizard?step=2&skip=postgres" style="font-size:13px">Пропустить (буду работать в режиме in-memory — без сохранения сделок)</a></p>{{end}}
{{if not .Done.Postgres}}<p style="margin-top:8px"><a href="/admin/wizard?step=4&skip=postgres" style="font-size:13px">Пропустить (буду работать в режиме in-memory — без сохранения сделок)</a></p>{{end}}
</div>
{{end}}
{{/* ============= ШАГ 2: Крипто (Валидата) ============= */}}
{{if eq .Step 2}}
{{if eq .Step 4}}
<div class="card">
<h2><span class="dot {{if .Done.Crypto}}ok{{else}}err{{end}}"></span>Шаг 2. СКЗИ «Валидата Клиент L»</h2>
<h2><span class="dot {{if .Done.Crypto}}ok{{else}}err{{end}}"></span>Шаг 4. СКЗИ «Валидата Клиент L»</h2>
<p>СКЗИ нужен для подписи XMLDSig и проверки квитанций НРД.</p>
<div class="help-block">
@@ -106,16 +157,16 @@
</details>
<div style="margin-top:20px;display:flex;justify-content:space-between">
<a href="/admin/wizard?step=1" class="btn" style="background:var(--card);text-decoration:none">← К шагу 1</a>
{{if .Done.Crypto}}<a href="/admin/wizard?step=3" class="btn" style="text-decoration:none">К шагу 3 →</a>{{else}}<a href="/admin/wizard?step=3&skip=crypto" class="btn" style="background:var(--card);text-decoration:none">Пропустить →</a>{{end}}
<a href="/admin/wizard?step=2" class="btn" style="background:var(--card);text-decoration:none">← К шагу 1</a>
{{if .Done.Crypto}}<a href="/admin/wizard?step=5" class="btn" style="text-decoration:none">К шагу 3 →</a>{{else}}<a href="/admin/wizard?step=5&skip=crypto" class="btn" style="background:var(--card);text-decoration:none">Пропустить →</a>{{end}}
</div>
</div>
{{end}}
{{/* ============= ШАГ 3: Сертификаты ============= */}}
{{if eq .Step 3}}
{{if eq .Step 5}}
<div class="card">
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Шаг 3. Сертификаты</h2>
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Шаг 5. Сертификаты</h2>
<p>Импортируйте сертификаты вашей организации и сертификаты УЦ НРД (для проверки квитанций).</p>
<div class="help-block">
@@ -173,16 +224,16 @@
{{end}}
<div style="margin-top:20px;display:flex;justify-content:space-between">
<a href="/admin/wizard?step=2" class="btn" style="background:var(--card);text-decoration:none">← К шагу 2</a>
<a href="/admin/wizard?step=4" class="btn" style="text-decoration:none">К шагу 4 →</a>
<a href="/admin/wizard?step=4" class="btn" style="background:var(--card);text-decoration:none">← К шагу 2</a>
<a href="/admin/wizard?step=6" class="btn" style="text-decoration:none">К шагу 4 →</a>
</div>
</div>
{{end}}
{{/* ============= ШАГ 4: НРД ============= */}}
{{if eq .Step 4}}
{{if eq .Step 6}}
<div class="card">
<h2><span class="dot {{if .Done.NSD}}ok{{else}}err{{end}}"></span>Шаг 4. Интеграционный шлюз НРД</h2>
<h2><span class="dot {{if .Done.NSD}}ok{{else}}err{{end}}"></span>Шаг 6. Интеграционный шлюз НРД</h2>
<p>Адрес web-сервиса ONYX и имя ключевого контейнера НРД.</p>
<div class="help-block">
@@ -203,7 +254,7 @@
</div>
<div>
<label>URL ONYX <span class="tooltip" title="Базовый URL веб-сервиса ONYX. При выборе профиля выше — заполняется автоматически.">?</span></label>
<input type="text" name="igw_url" id="nsd-url" value="{{.Settings.NSD.IGWBaseURL}}" placeholder="https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:100%">
<input type="text" name="igw_base_url" id="nsd-url" value="{{.Settings.NSD.IGWBaseURL}}" placeholder="https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:100%">
</div>
<div>
<label>Ключевой контейнер НРД <span class="tooltip" title="Имя контейнера Валидаты с ключами ЭДО НРД (выдаются УЦ НРД)">?</span></label>
@@ -220,7 +271,7 @@
</form>
{{if .Suggest}}{{if .Suggest.OrgName}}<form method="post" action="/admin/setup/org" style="margin-top:12px;display:grid;gap:10px">
<p class="muted" style="margin:0">Организация (из DEPO — проверьте): код участника, ИНН, наименование.</p>
<input type="hidden" name="return_to" value="/admin/wizard?step=4">
<input type="hidden" name="return_to" value="/admin/wizard?step=6">
<div><label>Код участника</label><input type="text" name="org_code" value="{{if .Settings.NSD.OrgCode}}{{.Settings.NSD.OrgCode}}{{else}}{{.Suggest.OrgCode}}{{end}}" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:100%"></div>
<div><label>ИНН</label><input type="text" name="org_inn" value="{{if .Settings.NSD.OrgINN}}{{.Settings.NSD.OrgINN}}{{else}}{{.Suggest.OrgINN}}{{end}}" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:100%"></div>
<div><label>Наименование</label><input type="text" name="org_name" value="{{if .Settings.NSD.OrgName}}{{.Settings.NSD.OrgName}}{{else}}{{.Suggest.OrgName}}{{end}}" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:100%"></div>
@@ -242,17 +293,17 @@
</script>
<div style="margin-top:20px;display:flex;justify-content:space-between">
<a href="/admin/wizard?step=3" class="btn" style="background:var(--card);text-decoration:none">← К шагу 3</a>
{{if .Done.NSD}}<a href="/admin/wizard?step=5" class="btn" style="text-decoration:none">К шагу 5 →</a>{{else}}<a href="/admin/wizard?step=5&skip=nsd" class="btn" style="background:var(--card);text-decoration:none">Пропустить ({{if devOn}}mock-режим{{else}}обмен с НРД не настроен{{end}}) →</a>{{end}}
<a href="/admin/wizard?step=5" class="btn" style="background:var(--card);text-decoration:none">← К шагу 3</a>
{{if .Done.NSD}}<a href="/admin/wizard?step=7" class="btn" style="text-decoration:none">К шагу 5 →</a>{{else}}<a href="/admin/wizard?step=7&skip=nsd" class="btn" style="background:var(--card);text-decoration:none">Пропустить ({{if devOn}}mock-режим{{else}}обмен с НРД не настроен{{end}}) →</a>{{end}}
</div>
</div>
{{end}}
{{/* ============= ШАГ 5: Тест-ран (режим разработки) / проверка подключений ============= */}}
{{if eq .Step 5}}
{{if eq .Step 7}}
<div class="card">
{{if devOn}}
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Шаг 5. Тестовая заявка</h2>
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Шаг 7. Тестовая заявка</h2>
<p>Прогон полного цикла: создание заявки → валидация → подпись → отправка в НРД (или mock) → ожидание Decision → подтверждение.</p>
<div class="help-block">
@@ -273,7 +324,7 @@
</table>
{{end}}
{{else}}
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Шаг 5. Проверка подключений</h2>
<h2><span class="dot {{if .Done.TestRun}}ok{{else}}err{{end}}"></span>Шаг 7. Проверка подключений</h2>
<p>Проверьте, что база данных, СКЗИ и шлюз НРД доступны: на странице «Статус» bj проверяет каждое подключение и показывает, что нужно исправить.</p>
<a href="/admin/status" class="btn" style="background:var(--ok);font-size:15px;padding:10px 20px;text-decoration:none;display:inline-block;margin-top:12px">Открыть проверку подключений →</a>
{{end}}
@@ -288,7 +339,7 @@
</table>
<div style="margin-top:20px;display:flex;justify-content:space-between">
<a href="/admin/wizard?step=4" class="btn" style="background:var(--card);text-decoration:none">← К шагу 4</a>
<a href="/admin/wizard?step=6" class="btn" style="background:var(--card);text-decoration:none">← К шагу 4</a>
<a href="/admin/" class="btn" style="text-decoration:none">Перейти к дашборду</a>
</div>
</div>