Ключ подписи ставится прямо в мастере (шаг «Ключ подписи»), а не на отдельной странице

- Мастер установки ключа — общий блок part_keywiz.html: на шаге мастера и на
  странице /admin/setup/keywizard. Формы возвращают туда, откуда открыли (ret=
  wizard), с выбранным контуром; вид — как у мастера.
- «Установить ключ» у канала на шаге «Шлюз НРД» ведёт на шаг «Ключ подписи» с
  контуром этого канала.
- Подсказка «сначала установите шлюз» — по установленному ИШ (раньше — по
  готовности шлюза, которая сама требует ключа).

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-06 17:50:34 +03:00
parent 71be8f2052
commit 5af3c5e3ba
7 changed files with 183 additions and 121 deletions
+40 -15
View File
@@ -105,6 +105,39 @@ type keyWizardData struct {
Drives []flashDrive
SuggestContour string // контур по сертификату в архиве: test3 (издатель «Тестовый УЦ») | prod | "" (не понятно)
SuggestIssuer string
WantContour string // контур, для которого открыли установку (кнопка «Установить ключ» у канала)
InWizard bool // блок — на шаге мастера: формы возвращают туда же
}
// keyWizView — данные блока установки ключа (want — контур, у которого нажали «Установить ключ»).
func (h *setupHandlers) keyWizView(want string, inWizard bool) *keyWizardData {
if h.keyWiz == nil { // обработчики без мастера ключа (тесты)
h.keyWiz = newKeyWizardState()
}
h.keyWiz.mu.Lock()
st := h.keyWiz
h.keyWiz.mu.Unlock()
d := &keyWizardData{State: st, InWizard: inWizard}
if want == "test3" || want == "prod" {
d.WantContour = want
}
if st.StagingID != "" && !st.Done { // список флешек — на шаге выбора носителя (архив уже загружен)
d.Drives = listFlashDrives()
d.SuggestContour, d.SuggestIssuer = keyArchiveContour(filepath.Join("/var/lib/bj/media/iso", st.StagingID))
}
return d
}
// keyWizBack — куда вернуться после действия: на шаг мастера (ret=wizard) или на отдельную страницу.
func keyWizBack(r *http.Request) string {
u := "/admin/setup/keywizard?"
if r.FormValue("ret") == "wizard" {
u = "/admin/wizard?step=8&"
}
if c := r.FormValue("want"); c == "test3" || c == "prod" {
u += "contour=" + c + "&"
}
return u
}
// keyArchiveContour — контур ключа по сертификатам в распакованном архиве (не самоподписанным — это УЦ):
@@ -229,25 +262,17 @@ func (h *setupHandlers) renderKeyWizard(w http.ResponseWriter, r *http.Request)
}
h.keyWiz.mu.Lock()
h.keyWiz.Flash = r.URL.Query().Get("flash")
st := h.keyWiz
h.keyWiz.mu.Unlock()
// Список флешек нужен на шаге выбора носителя (когда архив уже загружен).
var drives []flashDrive
if st.StagingID != "" && !st.Done {
drives = listFlashDrives()
}
d := keyWizardData{page: nowPage("Установка ключа", "setup"), State: st, Drives: drives}
if st.StagingID != "" && !st.Done {
d.SuggestContour, d.SuggestIssuer = keyArchiveContour(filepath.Join("/var/lib/bj/media/iso", st.StagingID))
}
render(w, h.tpl.a.keyWizard, d)
d := h.keyWizView(r.URL.Query().Get("contour"), false)
d.page = nowPage("Установка ключа", "setup")
render(w, h.tpl.a.keyWizard, *d)
}
func (h *setupHandlers) keyWizardReset(w http.ResponseWriter, r *http.Request) {
h.keyWiz.mu.Lock()
h.keyWiz.reset()
h.keyWiz.mu.Unlock()
http.Redirect(w, r, "/admin/setup/keywizard", http.StatusSeeOther)
http.Redirect(w, r, strings.TrimRight(keyWizBack(r), "?&"), http.StatusSeeOther)
}
// keyWizardUpload — шаг 1: приём .7z + пароль, распаковка, инспекция.
@@ -320,7 +345,7 @@ func (h *setupHandlers) keyWizardUpload(w http.ResponseWriter, r *http.Request)
h.keyWiz.set(1, "active", "")
h.keyWiz.mu.Unlock()
http.Redirect(w, r, "/admin/setup/keywizard", http.StatusSeeOther)
http.Redirect(w, r, strings.TrimRight(keyWizBack(r), "?&"), http.StatusSeeOther)
}
// keyWizardInstall — шаги 2-5: запись на флешку, справочник, проверка, готово.
@@ -427,7 +452,7 @@ func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request)
if dev {
final += " Финальная проверка — тестовым документом роботу."
}
http.Redirect(w, r, "/admin/setup/keywizard?flash="+urlQ(final), http.StatusSeeOther)
http.Redirect(w, r, keyWizBack(r)+"flash="+urlQ(final), http.StatusSeeOther)
}
// restartAndVerifyISH перезапускает Интеграционный шлюз и проверяет, что он
@@ -635,7 +660,7 @@ func runKeymediaOp(ctx context.Context, action string, fields map[string]string)
}
func (h *setupHandlers) keyWizFlash(w http.ResponseWriter, r *http.Request, msg string) {
http.Redirect(w, r, "/admin/setup/keywizard?flash="+urlQ(msg), http.StatusSeeOther)
http.Redirect(w, r, keyWizBack(r)+"flash="+urlQ(msg), http.StatusSeeOther)
}
func fallback(s, def string) string {
+16
View File
@@ -758,3 +758,19 @@ func TestIgateInstallStatus(t *testing.T) {
t.Fatalf("готово: %+v", st)
}
}
// Установка ключа из мастера возвращает на шаг «Ключ подписи» (с выбранным контуром), со страницы — на страницу.
func TestKeyWizBack(t *testing.T) {
for form, want := range map[string]string{
"ret=wizard&want=prod": "/admin/wizard?step=8&contour=prod&",
"ret=wizard": "/admin/wizard?step=8&",
"": "/admin/setup/keywizard?",
"want=evil": "/admin/setup/keywizard?",
} {
r := httptest.NewRequest(http.MethodPost, "/admin/setup/keywizard/reset", strings.NewReader(form))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if got := keyWizBack(r); got != want {
t.Errorf("%q: %q, ожидалось %q", form, got, want)
}
}
}
+5
View File
@@ -185,6 +185,8 @@ type WizardData struct {
AutoDone []string // шаги до текущего, уже выполненные без участия пользователя, — с пояснением
Certs []cryptocli.Certificate
Flash string
KeyWiz *keyWizardData // шаг «Ключ подписи»: установка ключа прямо в мастере
IgateOK bool // ИШ на сервере установлен (ключ ставится в его профиль)
// главы мастера (wizchapters.go): путь этой установки, соседние шаги, шапка
Path []int
Chapters []WizChapter
@@ -322,6 +324,9 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
fd.InWizard = true
d.Fancy = &fd
}
if step == 8 {
d.KeyWiz, d.IgateOK = h.keyWizView(r.URL.Query().Get("contour"), true), igateInstalled()
}
if step == 3 {
d.Sub = depoWizardSub(r.URL.Query().Get("sub"))
d.DepoUser, d.DepoPass = depoSetupLogin(s.Fancy), s.Fancy.Password
@@ -6,109 +6,7 @@
<span class="hero-status">Загрузите архив НРД → запись на носитель → справочник сертификатов → проверка → готово</span>
</div>
{{$s := .State}}
{{/* ===== Лента шагов ===== */}}
<div class="card">
<ol style="list-style:none;padding:0;margin:0;display:grid;gap:12px">
{{range $i, $step := $s.Steps}}
<li style="display:flex;gap:12px;align-items:flex-start">
<span style="flex:0 0 28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;
{{if eq $step.Status "ok"}}background:var(--ok-weak);color:var(--ok)
{{else if eq $step.Status "error"}}background:var(--err-weak);color:var(--err)
{{else if eq $step.Status "active"}}background:var(--accent-weak);color:var(--accent)
{{else}}background:var(--surface-2,#eee);color:var(--muted,#999){{end}}">
{{if eq $step.Status "ok"}}✓{{else if eq $step.Status "error"}}{{else}}{{add $i 1}}{{end}}
</span>
<div style="flex:1">
<div style="font-weight:600">{{$step.Title}}</div>
{{if $step.Detail}}<div class="muted" style="font-size:13px;margin-top:2px">{{$step.Detail}}</div>{{end}}
</div>
</li>
{{end}}
</ol>
</div>
{{/* ===== Действие в зависимости от состояния ===== */}}
{{if $s.Done}}
<div class="card" style="background:var(--ok-weak)">
<h2>✓ Готово</h2>
<p>Ключ установлен на флешку, справочник сертификатов сформирован, Валидата проверена.</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}}<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">
<button type="submit" class="btn btn-secondary">Установить ещё один ключ</button>
</form>
</div>
</div>
{{else if $s.StagingID}}
{{/* Архив загружен — выбор флешки + запись */}}
<div class="card">
<h2>Шаг 2 — выбор флешки и запись</h2>
<p class="muted">Архив распакован. Ключ: <code>{{fallbackTpl $s.VDK "—"}}</code>.
Выберите носитель — запись сделает бэкап, запишет ключ и справочник
сертификатов, дотянет CRL и перезапустит ИШ.</p>
<form method="post" action="/admin/setup/keywizard/install" style="margin-top:12px;display:grid;gap:12px;max-width:640px"
onsubmit="this.querySelector('button[type=submit]').disabled=true;this.querySelector('button[type=submit]').textContent='Устанавливаю…';">
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Целевая флешка</label>
{{if .Drives}}
{{range $i, $d := .Drives}}
<label style="display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;margin-bottom:8px;cursor:pointer">
<input type="radio" name="target_device" value="{{$d.Device}}" {{if $d.IsKeymedia}}checked{{else if and (eq $i 0) (not (anyKeymedia $.Drives))}}checked{{end}} style="margin-top:3px">
<span>
<b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}
{{if $d.Label}}· метка «{{$d.Label}}»{{end}}<br>
<span class="muted" style="font-size:12px">{{$d.Device}}{{if $d.Mountpoint}} · {{$d.Mountpoint}}{{end}}
{{if $d.IsKeymedia}}<b style="color:var(--accent)">← текущий ключевой носитель ИШ (рекомендуется)</b>{{end}}</span>
</span>
</label>
{{end}}
{{else}}
<p class="muted">Съёмные носители не обнаружены — будет использован текущий ключевой носитель ИШ по умолчанию.</p>
{{end}}
</div>
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Для какого контура НРД этот ключ</label>
{{$c := .SuggestContour}}{{if not $c}}{{$c = currentContour}}{{end}}
{{if .SuggestIssuer}}<p class="muted" style="font-size:12px;margin:0 0 4px">В архиве сертификат от «{{.SuggestIssuer}}» — это {{if eq .SuggestContour "test3"}}тестовый{{else}}боевой{{end}} ключ.</p>{{end}}
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="test3" style="width:auto" {{if ne $c "prod"}}checked{{end}}> Тестовый TEST3 <span class="muted" style="font-size:12px">— сертификат от «Тестовый УЦ»</span></label>
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="prod" style="width:auto" {{if eq $c "prod"}}checked{{end}}> Боевой <span class="muted" style="font-size:12px">— выдан УЦ Московской биржи для работы</span></label>
<span class="muted" style="font-size:12px">У тестового и боевого контуров разные ключи: ключ подключается только к каналу ИШ своего контура.</span>
</div>
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Имя профиля в справочнике (необязательно)</label>
<input type="text" name="profile_name" placeholder="Авто из архива (напр. PrUser1046)" autocomplete="off"
pattern="[A-Za-z0-9_-]*" style="width:100%">
<span class="muted" style="font-size:12px">Пусто = имя берётся из архива автоматически.</span>
</div>
<button type="submit" class="btn btn-ok">Записать на флешку, сформировать справочник и проверить ИШ</button>
</form>
<form method="post" action="/admin/setup/keywizard/reset" style="margin-top:8px">
<button type="submit" class="btn btn-secondary">Отмена / загрузить другой архив</button>
</form>
</div>
{{else}}
{{/* Начало — форма загрузки */}}
<div class="card">
<h2>Шаг 1 — загрузка архива</h2>
<p class="muted">Выберите .7z-архив с ключом от НРД и введите пароль архива.</p>
<form method="post" action="/admin/setup/keywizard/upload" enctype="multipart/form-data"
style="margin-top:12px;display:grid;gap:10px;max-width:560px">
<input type="file" name="archive" accept=".7z,.zip" required>
<input type="password" name="password" placeholder="Пароль архива (например 11)" autocomplete="off">
<button type="submit" class="btn btn-ok">Загрузить и распаковать</button>
</form>
</div>
{{end}}
{{template "keyWizBody" .}}
<p style="margin-top:16px"><a href="/admin/setup" class="muted">← Назад к настройкам</a></p>
@@ -86,6 +86,13 @@
.dq-jobname { margin-top:14px; max-width:520px; }
.dq-jobname label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
.dq-jobname input { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
/* установка ключа (общий блок part_keywiz) — в виде мастера */
.keywiz ol { gap:4px !important; }
.keywiz ol li > div > div:first-child { font-weight:500 !important; font-size:14.5px; }
.keywiz ol li > span:first-child { flex-basis:24px !important; height:24px !important; font-size:12px !important; }
.keywiz .card h2 { font-size:16px; margin:0 0 6px; }
.keywiz form .btn-ok, .keywiz form .btn-secondary { justify-self:start; border-radius:999px; padding:10px 22px; font-weight:600; }
.keywiz form .btn-ok { background:var(--accent); border-color:var(--accent); }
.dq-box { border:1px solid var(--border); border-radius:12px; padding:14px 16px 16px; }
.dq-box-bj { background:color-mix(in srgb, var(--accent-weak) 45%, transparent); }
.dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; }
@@ -556,8 +563,8 @@
<div class="card">
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Ключ и сертификаты</h2>
<p>Ключ электронной подписи организации для обмена с НРД. Его выдаёт Удостоверяющий центр Московской биржи: вам передадут архив ключа и пароль.</p>
{{if not .Done.NSD}}<div class="wiz-info"><span>Ключ ставится в шлюз НРД — сначала установите <a href="/admin/wizard?step=7">шлюз</a>.</span></div>{{end}}
<p style="margin:14px 0 0"><a href="/admin/setup/keywizard" class="btn" style="text-decoration:none;display:inline-block">{{if .Certs}}Установить другой ключ{{else}}Установить ключ{{end}}</a> <span class="muted" style="font-size:12.5px;margin-left:6px">архив → носитель → справочник → проверка подписи</span></p>
{{if not .IgateOK}}<div class="wiz-info"><span>Ключ ставится в шлюз НРД — сначала установите <a href="/admin/wizard?step=7">шлюз</a>.</span></div>
{{else if .KeyWiz}}<div class="keywiz">{{template "keyWizBody" .KeyWiz}}</div>{{end}}
<div class="wiz-row" style="margin-top:20px">
<span class="wiz-row-l">Сертификаты НРД</span>
@@ -93,7 +93,7 @@
s.push(ch.profile?ok('ключ: профиль '+ch.profile):bad('ключ этого контура не установлен'));
s.push(ch.enabled?ok('канал включён'):'<span class="muted">канал выключен до установки ключа</span>');
a.push('<button type="button" class="btn sm ghost" data-act="contour" data-c="'+x[0]+'">Обновить канал</button>');
if(!ch.profile) a.push('<a class="btn sm" href="/admin/setup/keywizard" style="text-decoration:none">Установить ключ</a>');
if(!ch.profile) a.push('<a class="btn sm" href="/admin/wizard?step=8&contour='+x[0]+'" style="text-decoration:none">Установить ключ</a>');
if(ch.enabled && !act) a.push(x[0]==='prod'?'<button type="button" class="btn sm" data-act="showprod">Перейти на боевой…</button>':'<button type="button" class="btn sm" data-act="switch" data-c="test3">Сделать активным</button>');
}
return '<tr><td style="white-space:nowrap;width:130px"><b>'+x[1]+'</b>'+(act?'<br><span style="color:var(--accent);font-size:12px">● активный</span>':'')+'</td><td>'+s.join('<br>')+(a.length?'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">'+a.join('')+'</div>':'')+'</td></tr>';
@@ -0,0 +1,111 @@
{{/* Установка ключа Валидаты: загрузка архива НРД → носитель → справочник → проверка ИШ. Один и тот же блок —
на шаге «Ключ подписи» мастера и на отдельной странице /admin/setup/keywizard (данные — keyWizardData). */}}
{{define "keyWizRet"}}{{if .InWizard}}<input type="hidden" name="ret" value="wizard">{{end}}{{if .WantContour}}<input type="hidden" name="want" value="{{.WantContour}}">{{end}}{{end}}
{{define "keyWizBody"}}
{{$s := .State}}
{{/* ===== Лента шагов ===== */}}
<div class="card">
<ol style="list-style:none;padding:0;margin:0;display:grid;gap:12px">
{{range $i, $step := $s.Steps}}
<li style="display:flex;gap:12px;align-items:flex-start">
<span style="flex:0 0 28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;
{{if eq $step.Status "ok"}}background:var(--ok-weak);color:var(--ok)
{{else if eq $step.Status "error"}}background:var(--err-weak);color:var(--err)
{{else if eq $step.Status "active"}}background:var(--accent-weak);color:var(--accent)
{{else}}background:var(--surface-2,#eee);color:var(--muted,#999){{end}}">
{{if eq $step.Status "ok"}}✓{{else if eq $step.Status "error"}}{{else}}{{add $i 1}}{{end}}
</span>
<div style="flex:1">
<div style="font-weight:600">{{$step.Title}}</div>
{{if $step.Detail}}<div class="muted" style="font-size:13px;margin-top:2px">{{$step.Detail}}</div>{{end}}
</div>
</li>
{{end}}
</ol>
</div>
{{/* ===== Действие в зависимости от состояния ===== */}}
{{if $s.Done}}
<div class="card" style="background:var(--ok-weak)">
<h2>✓ Готово</h2>
<p>Ключ установлен на флешку, справочник сертификатов сформирован, Валидата проверена.</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>
</div>
{{else if $s.StagingID}}
{{/* Архив загружен — выбор флешки + запись */}}
<div class="card">
<h2>Шаг 2 — выбор флешки и запись</h2>
<p class="muted">Архив распакован. Ключ: <code>{{fallbackTpl $s.VDK "—"}}</code>.
Выберите носитель — запись сделает бэкап, запишет ключ и справочник
сертификатов, дотянет CRL и перезапустит ИШ.</p>
<form method="post" action="/admin/setup/keywizard/install" style="margin-top:12px;display:grid;gap:12px;max-width:640px"
onsubmit="this.querySelector('button[type=submit]').disabled=true;this.querySelector('button[type=submit]').textContent='Устанавливаю… (до нескольких минут)';">
{{template "keyWizRet" .}}
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Целевая флешка</label>
{{if .Drives}}
{{range $i, $d := .Drives}}
<label style="display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;margin-bottom:8px;cursor:pointer">
<input type="radio" name="target_device" value="{{$d.Device}}" {{if $d.IsKeymedia}}checked{{else if and (eq $i 0) (not (anyKeymedia $.Drives))}}checked{{end}} style="margin-top:3px">
<span>
<b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}
{{if $d.Label}}· метка «{{$d.Label}}»{{end}}<br>
<span class="muted" style="font-size:12px">{{$d.Device}}{{if $d.Mountpoint}} · {{$d.Mountpoint}}{{end}}
{{if $d.IsKeymedia}}<b style="color:var(--accent)">← текущий ключевой носитель ИШ (рекомендуется)</b>{{end}}</span>
</span>
</label>
{{end}}
{{else}}
<p class="muted">Съёмные носители не обнаружены — будет использован текущий ключевой носитель ИШ по умолчанию.</p>
{{end}}
</div>
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Для какого контура НРД этот ключ</label>
{{$c := .SuggestContour}}{{if not $c}}{{$c = .WantContour}}{{end}}{{if not $c}}{{$c = currentContour}}{{end}}
{{if .SuggestIssuer}}<p class="muted" style="font-size:12px;margin:0 0 4px">В архиве сертификат от «{{.SuggestIssuer}}» — это {{if eq .SuggestContour "test3"}}тестовый{{else}}боевой{{end}} ключ.</p>{{end}}
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="test3" style="width:auto" {{if ne $c "prod"}}checked{{end}}> Тестовый TEST3 <span class="muted" style="font-size:12px">— сертификат от «Тестовый УЦ»</span></label>
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="prod" style="width:auto" {{if eq $c "prod"}}checked{{end}}> Боевой <span class="muted" style="font-size:12px">— выдан УЦ Московской биржи для работы</span></label>
<span class="muted" style="font-size:12px">У тестового и боевого контуров разные ключи: ключ подключается только к каналу ИШ своего контура.</span>
</div>
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Имя профиля в справочнике (необязательно)</label>
<input type="text" name="profile_name" placeholder="Авто из архива (напр. PrUser1046)" autocomplete="off"
pattern="[A-Za-z0-9_-]*" style="width:100%">
<span class="muted" style="font-size:12px">Пусто = имя берётся из архива автоматически.</span>
</div>
<button type="submit" class="btn btn-ok">Записать на флешку, сформировать справочник и проверить ИШ</button>
</form>
<form method="post" action="/admin/setup/keywizard/reset" style="margin-top:8px">{{template "keyWizRet" .}}
<button type="submit" class="btn btn-secondary">Отмена / загрузить другой архив</button>
</form>
</div>
{{else}}
{{/* Начало — форма загрузки */}}
<div class="card">
<h2>Шаг 1 — загрузка архива</h2>
<p class="muted">Выберите .7z-архив с ключом от НРД и введите пароль архива.</p>
<form method="post" action="/admin/setup/keywizard/upload" enctype="multipart/form-data"
style="margin-top:12px;display:grid;gap:10px;max-width:560px">
{{template "keyWizRet" .}}
<input type="file" name="archive" accept=".7z,.zip" required>
<input type="password" name="password" placeholder="Пароль архива (например 11)" autocomplete="off">
<button type="submit" class="btn btn-ok">Загрузить и распаковать</button>
</form>
</div>
{{end}}
{{end}}