Мастер: «Обновление» — первый пункт, к которому можно вернуться

Новый шаг 14 «Обновление» в отдельной первой главе мастера. Секция
«Обновления» вынесена из admin_setup.html в {{define "updateSection"}}
(part_fancy.html) и переиспользуется и в настройках, и в шаге мастера —
один источник правды. В мастере рендерится обычным блоком (без
settings-section/active), заголовок h1 скрыт (есть свой h2 шага).

Шаг необязательный (не блокирует путь), зелёный пока нет версии новее;
при наличии обновления — предупреждающая точка. Добавлена пятая глава,
поэтому wizChaptersLine расширен до «Пять глав».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX
This commit is contained in:
fontvielle
2026-10-19 17:22:29 +03:00
parent 2df7b0f54a
commit b7360b93b2
6 changed files with 110 additions and 93 deletions
+6 -6
View File
@@ -210,11 +210,11 @@ func TestWizardChaptersWithoutLicense(t *testing.T) {
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
done := map[int]bool{2: true, 6: true}
path, chapters := wizPath(rc, done, 4)
want := []int{1, 12, 4, 7, 8, 9, 13, 10} // без лицензии нет учёта DEPO, QUIK и модулей; база и Валидата — сделаны; учётные записи — перед проверкой
want := []int{14, 1, 12, 4, 7, 8, 9, 13, 10} // обновление — первый пункт; без лицензии нет учёта DEPO, QUIK и модулей; база и Валидата — сделаны; учётные записи — перед проверкой
if fmt.Sprint(path) != fmt.Sprint(want) {
t.Fatalf("путь %v, ожидался %v", path, want)
}
if len(chapters) != 4 || !chapters[1].Current || chapters[1].Steps[0].N != 4 {
if len(chapters) != 5 || !chapters[2].Current || chapters[2].Steps[0].N != 4 {
t.Fatalf("главы: %+v", chapters)
}
if p, n := wizNeighbours(path, 4); p != 12 || n != 7 {
@@ -233,10 +233,10 @@ func TestWizardChaptersWithoutLicense(t *testing.T) {
if len(wizModuleViews(rc, rc.Snapshot())) != 0 {
t.Fatal("без лицензии модулей в мастере быть не должно")
}
if chapters[1].Title != "Организация" {
t.Fatalf("без учёта DEPO и QUIK глава 2 — «Организация», а не %q", chapters[1].Title)
if chapters[2].Title != "Организация" {
t.Fatalf("без учёта DEPO и QUIK глава «Организация», а не %q", chapters[2].Title)
}
if l := wizChaptersLine(chapters); l != "Четыре главы: сервер, организация, НРД, запуск." {
if l := wizChaptersLine(chapters); l != "Пять глав: обновление, сервер, организация, НРД, запуск." {
t.Fatalf("подзаголовок: %q", l)
}
}
@@ -437,7 +437,7 @@ func TestWizardStepsRender(t *testing.T) {
t.Fatal(err)
}
h := &setupHandlers{rc: rc, tpl: &adminTemplates{a: a}}
for step, want := range map[string]string{"1": "QUIK: неторговые поручения", "3": "wiz-depo", "5": "Сохранить и проверить", "12": "wiz-tz-suggest", "10": "Проверка"} {
for step, want := range map[string]string{"1": "QUIK: неторговые поручения", "3": "wiz-depo", "5": "Сохранить и проверить", "12": "wiz-tz-suggest", "10": "Проверка", "14": "sec-update"} {
w := httptest.NewRecorder()
h.renderWizard(w, httptest.NewRequest(http.MethodGet, "/admin/wizard?step="+step, nil))
body := w.Body.String()
+4 -3
View File
@@ -288,8 +288,9 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
// Главы (wizchapters.go): в пути только шаги, нужные этой установке — по лицензии и без сделанного
// установщиком. Без ?step — первый несделанный обязательный шаг, а если всё сделано — проверка.
d.Done.Users = doneUsers()
_, wizUpdAvail := updateComponentRows(h.rc) // шаг 14 «Обновление»: зелёный, пока нет версии новее
done := map[int]bool{1: d.Done.License, 2: d.Done.Postgres, 3: d.Done.Depo, 4: d.Done.Org, 5: d.Done.Quik, 6: d.Done.Crypto,
7: d.Done.NSD, 8: d.Done.Certs, 9: d.Done.Alerts, 10: d.Done.TestRun, 11: d.Done.Modules, 12: d.Done.Server, 13: d.Done.Users}
7: d.Done.NSD, 8: d.Done.Certs, 9: d.Done.Alerts, 10: d.Done.TestRun, 11: d.Done.Modules, 12: d.Done.Server, 13: d.Done.Users, 14: !wizUpdAvail}
path, _ := wizPath(h.rc, done, 0)
step := wizDefaultStep(path, done)
if v := r.URL.Query().Get("step"); v != "" {
@@ -327,7 +328,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
}
cancel()
}
if (step >= 3 && step <= 5) || step == 9 || step == 11 || step == 13 { // DEPO, организация, QUIK МНП, оповещения и копии, модули, учётные записи — общие блоки из «Настройки»
if (step >= 3 && step <= 5) || step == 9 || step == 11 || step == 13 || step == 14 { // DEPO, организация, QUIK МНП, оповещения и копии, модули, учётные записи, обновление — общие блоки из «Настройки»
fd := h.setupData(w, r, "")
fd.InWizard = true
d.Fancy = &fd
@@ -398,7 +399,7 @@ func depoServerEdition(ctx context.Context, f FancyMSSQLSettings) (string, bool)
// wizardSteps — шагов в мастере: 1 лицензия, 2 база bj, 3 учёт DEPO, 4 наша организация, 5 QUIK МНП,
// 6 Валидата, 7 шлюз НРД, 8 ключ и сертификаты (ключ ставится в профиль ИШ — после шлюза),
// 9 оповещения и резервные копии, 10 проверка готовности.
const wizardSteps = 13 // 11 — модули по лицензии (глава «Запуск»), 12 — время и сеть (глава «Сервер»), 13 — учётные записи
const wizardSteps = 14 // 11 — модули, 12 — время и сеть, 13 — учётные записи, 14 — обновление (первый пункт, глава «Обновление»)
// dsnSummary — «host:port, база name» из строки подключения PostgreSQL, без логина и пароля.
func dsnSummary(dsn string) string {
@@ -936,88 +936,7 @@
</section>
{{/* ============ ОБНОВЛЕНИЯ ============ */}}
<section class="settings-section" id="sec-update">
<h1>Обновления</h1>
{{template "verline" (.Versions.Get "bj-server")}}
<div class="card" id="upd-status" data-last="{{if not .Settings.Update.LastCheck.IsZero}}{{.Settings.Update.LastCheck.Unix}}{{else}}0{{end}}" data-has="{{if .UpdatesAvailable}}1{{else}}0{{end}}">
{{if .UpdatesAvailable}}
<div class="stc stc-warn"><span class="stc-ic"></span><div class="stc-b"><b>Доступно обновление{{with .Settings.Update.Available}} {{.}}{{end}}</b><small>{{if .Settings.Update.Notes}}{{.Settings.Update.Notes}}{{else}}новые версии компонентов — список ниже, в «Компоненты и версии»{{end}}</small></div>
<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('Установить обновление? Службы, которые обновляются, перезапустятся на несколько секунд.');"><input type="hidden" name="section" value="sec-update"><button type="submit" class="btn">Установить обновление</button></form></div>
{{else if and (not .Settings.Update.LastCheck.IsZero) .UpdateFailed}}
<div class="stc stc-err"><span class="stc-ic"></span><div class="stc-b"><b>Проверить обновления не удалось</b><small>{{.Settings.Update.LastResult}} · {{msk .Settings.Update.LastCheck}}</small></div>
<button type="button" class="btn btn-secondary" data-upd-check>Проверить ещё раз</button></div>
{{else}}
<div class="stc stc-ok"><span class="stc-ic"></span><div class="stc-b"><b>Установлены последние версии</b><small id="upd-when">{{if .Settings.Update.LastCheck.IsZero}}ещё не проверялось{{else}}проверено {{msk .Settings.Update.LastCheck}}{{end}}{{if .Settings.Update.AutoCheck}} · bj проверяет сам каждые 6 часов{{end}}</small></div>
<button type="button" class="btn btn-secondary" data-upd-check>Проверить сейчас</button></div>
{{end}}
{{if and .License.Present (not .License.AllowsUpdates)}}<p class="muted" style="margin-top:6px;font-size:12px;color:var(--warn)">В лицензии нет модуля «Обновления».</p>{{end}}
</div>
<script>(function(){
var box=document.getElementById('upd-status'), sec=document.getElementById('sec-update'), busy=false;
function check(force){
if(busy) return; var last=+box.dataset.last||0, now=Date.now()/1000, tried=0;
try{ tried=+sessionStorage.getItem('bj-upd-auto')||0; }catch(e){}
if(!force && (now-last<600 || now-tried<600)) return; // проверяли меньше 10 минут назад (или уже пробовали сами)
try{ sessionStorage.setItem('bj-upd-auto',String(Math.floor(now))); }catch(e){}
busy=true; var b=box.querySelector('.stc'); if(b){ b.className='stc stc-run'; var t=b.querySelector('b'); if(t) t.textContent='Проверяю обновления…'; }
fetch('/admin/setup/update/check',{method:'POST',headers:{'X-BJ-Ajax':'1'},cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
box.dataset.last=String(Math.floor(Date.now()/1000));
location.reload(); // состояние компонентов — с сервера
}).catch(function(){ busy=false; location.reload(); });
}
box.addEventListener('click',function(e){ if(e.target.closest('[data-upd-check]')) check(true); });
document.querySelectorAll('.settings-nav [data-sec="update"]').forEach(function(b){ b.addEventListener('click',function(){ setTimeout(function(){ check(false); },0); }); });
window.addEventListener('load',function(){ if(sec.classList.contains('active')) check(false); });
})();</script>
<div class="card">
<details class="adv-box" {{if .UpdatesAvailable}}open{{end}}>
<summary><span class="sum-h">Компоненты и версии</span><span class="muted">что установлено на этом сервере и что есть в выпуске; можно обновить один компонент</span></summary>
<div style="overflow-x:auto"><table style="margin-top:8px">
<thead><tr><th>Компонент</th><th>Установлена</th><th>Доступна</th><th>Служба</th><th></th></tr></thead>
<tbody>
{{range .Components}}<tr>
<td><strong>{{.Title}}</strong><div class="muted" style="font-size:11px"><code>{{.Name}}</code>{{if eq .Name "crypto-service.jar"}}{{with $.Versions.Get "validata"}}{{if .Version}} · работает с «Валидата Клиент L» {{.Version}}{{end}}{{end}}{{end}}</div></td>
<td>{{if .Present}}{{if .Installed}}{{.Installed}}{{else}}<span class="muted">неизвестна</span>{{end}}{{else}}<span class="muted">не установлен</span>{{end}}</td>
<td>{{if .Available}}{{.Available}}{{if .New}} <span class="hero-status warn">к установке</span>{{else if .Update}} <span class="hero-status warn">новее</span>{{end}}{{if .Blocked}}<div class="muted" style="font-size:11px;color:var(--warn)">{{.Blocked}}</div>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
<td><code style="font-size:11px">{{.Unit}}</code></td>
<td>{{if .Update}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('{{if .New}}Установить модуль {{.Available}}? Будет включена его служба.{{else}}Обновить компонент до {{.Available}}? Его служба перезапустится.{{end}}');"><input type="hidden" name="section" value="sec-update"><input type="hidden" name="component" value="{{.Name}}"><button type="submit" class="btn btn-ok">{{if .New}}Установить{{else}}Обновить{{end}}</button></form>{{end}}</td>
</tr>{{end}}
</tbody>
</table></div>
</details>
</div>
<div class="card">
<details class="adv-box">
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">источник и канал обновлений, обновление из файла для сервера без выхода в интернет</span></summary>
<h3 class="adv-h">Обновление из файла</h3>
<p class="muted" style="margin:0 0 8px;font-size:12.5px"><b>Закрытый контур.</b> Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.</p>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<form method="post" action="/admin/setup/update/upload" enctype="multipart/form-data" style="margin:0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" onsubmit="return this.bundle.files.length>0 && confirm('Установить обновление из файла? Подпись пакета будет проверена; поставятся компоненты новее текущих — сначала ядро, затем модули. Службы перезапустятся.');">
<input type="hidden" name="section" value="sec-update">
<input type="file" name="bundle" accept=".zip,application/zip" required style="max-width:260px">
<button type="submit" class="btn btn-secondary" title="Пакет обновления с портала клиента — для сервера без выхода к серверу обновлений">Обновить из файла</button>
</form>
</div>
<h3 class="adv-h">Источник обновлений</h3>
<form method="post" action="/admin/setup/update" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<label>URL артефактории</label>
<input type="text" name="base_url" value="{{.Settings.Update.BaseURL}}" placeholder="https://lic.zetit.ru/updates (по умолчанию)">
<label>Канал</label>
<select name="channel">
<option value="stable" {{if eq .Settings.Update.Channel "stable"}}selected{{end}}>stable — стабильный</option>
<option value="beta" {{if eq .Settings.Update.Channel "beta"}}selected{{end}}>beta — предварительный</option>
</select>
<label>Публичный ключ издателя (base64 Ed25519)</label>
<input type="text" name="public_key" value="{{.Settings.Update.PublicKey}}" placeholder="зашит в релиз; переопределить здесь">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="auto_check" {{if .Settings.Update.AutoCheck}}checked{{end}} style="width:auto"> Проверять автоматически (раз в 6 часов)</label>
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</details>
</div>
</section>
{{template "updateSection" .}}
{{/* ============ ЛИЦЕНЗИЯ ============ */}}
{{/* ============ РЕЗЕРВНОЕ КОПИРОВАНИЕ ============ */}}
@@ -611,6 +611,16 @@
</div>
{{end}}
{{/* ============= ШАГ 14: Обновление (глава «Обновление») ============= */}}
{{if eq .Step 14}}
<div class="card">
<h2><span class="dot {{if .StepDone}}ok{{else}}warn{{end}}"></span>Обновление</h2>
<p>Проверка и установка обновлений bj. Это первый пункт мастера — сюда можно вернуться в любой момент.</p>
{{with .Fancy}}{{template "updateSection" .}}{{end}}
{{template "wizNav" .}}
</div>
{{end}}
{{/* ============= ШАГ 11: Модули по лицензии (глава «Запуск») ============= */}}
{{if eq .Step 11}}
<div class="card">
@@ -137,3 +137,89 @@
{{end}}
</div>
{{end}}
{{/* Секция «Обновления»: общий блок для страницы настроек и шага 14 мастера. */}}
{{define "updateSection"}}
{{if .InWizard}}<div id="sec-update">{{else}}<section class="settings-section" id="sec-update">{{end}}
{{if not .InWizard}}<h1>Обновления</h1>{{end}}
{{template "verline" (.Versions.Get "bj-server")}}
<div class="card" id="upd-status" data-last="{{if not .Settings.Update.LastCheck.IsZero}}{{.Settings.Update.LastCheck.Unix}}{{else}}0{{end}}" data-has="{{if .UpdatesAvailable}}1{{else}}0{{end}}">
{{if .UpdatesAvailable}}
<div class="stc stc-warn"><span class="stc-ic"></span><div class="stc-b"><b>Доступно обновление{{with .Settings.Update.Available}} {{.}}{{end}}</b><small>{{if .Settings.Update.Notes}}{{.Settings.Update.Notes}}{{else}}новые версии компонентов — список ниже, в «Компоненты и версии»{{end}}</small></div>
<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('Установить обновление? Службы, которые обновляются, перезапустятся на несколько секунд.');"><input type="hidden" name="section" value="sec-update"><button type="submit" class="btn">Установить обновление</button></form></div>
{{else if and (not .Settings.Update.LastCheck.IsZero) .UpdateFailed}}
<div class="stc stc-err"><span class="stc-ic"></span><div class="stc-b"><b>Проверить обновления не удалось</b><small>{{.Settings.Update.LastResult}} · {{msk .Settings.Update.LastCheck}}</small></div>
<button type="button" class="btn btn-secondary" data-upd-check>Проверить ещё раз</button></div>
{{else}}
<div class="stc stc-ok"><span class="stc-ic"></span><div class="stc-b"><b>Установлены последние версии</b><small id="upd-when">{{if .Settings.Update.LastCheck.IsZero}}ещё не проверялось{{else}}проверено {{msk .Settings.Update.LastCheck}}{{end}}{{if .Settings.Update.AutoCheck}} · bj проверяет сам каждые 6 часов{{end}}</small></div>
<button type="button" class="btn btn-secondary" data-upd-check>Проверить сейчас</button></div>
{{end}}
{{if and .License.Present (not .License.AllowsUpdates)}}<p class="muted" style="margin-top:6px;font-size:12px;color:var(--warn)">В лицензии нет модуля «Обновления».</p>{{end}}
</div>
<script>(function(){
var box=document.getElementById('upd-status'), sec=document.getElementById('sec-update'), busy=false;
function check(force){
if(busy) return; var last=+box.dataset.last||0, now=Date.now()/1000, tried=0;
try{ tried=+sessionStorage.getItem('bj-upd-auto')||0; }catch(e){}
if(!force && (now-last<600 || now-tried<600)) return; // проверяли меньше 10 минут назад (или уже пробовали сами)
try{ sessionStorage.setItem('bj-upd-auto',String(Math.floor(now))); }catch(e){}
busy=true; var b=box.querySelector('.stc'); if(b){ b.className='stc stc-run'; var t=b.querySelector('b'); if(t) t.textContent='Проверяю обновления…'; }
fetch('/admin/setup/update/check',{method:'POST',headers:{'X-BJ-Ajax':'1'},cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
box.dataset.last=String(Math.floor(Date.now()/1000));
location.reload(); // состояние компонентов — с сервера
}).catch(function(){ busy=false; location.reload(); });
}
box.addEventListener('click',function(e){ if(e.target.closest('[data-upd-check]')) check(true); });
document.querySelectorAll('.settings-nav [data-sec="update"]').forEach(function(b){ b.addEventListener('click',function(){ setTimeout(function(){ check(false); },0); }); });
window.addEventListener('load',function(){ if(sec.classList.contains('active')) check(false); });
})();</script>
<div class="card">
<details class="adv-box" {{if .UpdatesAvailable}}open{{end}}>
<summary><span class="sum-h">Компоненты и версии</span><span class="muted">что установлено на этом сервере и что есть в выпуске; можно обновить один компонент</span></summary>
<div style="overflow-x:auto"><table style="margin-top:8px">
<thead><tr><th>Компонент</th><th>Установлена</th><th>Доступна</th><th>Служба</th><th></th></tr></thead>
<tbody>
{{range .Components}}<tr>
<td><strong>{{.Title}}</strong><div class="muted" style="font-size:11px"><code>{{.Name}}</code>{{if eq .Name "crypto-service.jar"}}{{with $.Versions.Get "validata"}}{{if .Version}} · работает с «Валидата Клиент L» {{.Version}}{{end}}{{end}}{{end}}</div></td>
<td>{{if .Present}}{{if .Installed}}{{.Installed}}{{else}}<span class="muted">неизвестна</span>{{end}}{{else}}<span class="muted">не установлен</span>{{end}}</td>
<td>{{if .Available}}{{.Available}}{{if .New}} <span class="hero-status warn">к установке</span>{{else if .Update}} <span class="hero-status warn">новее</span>{{end}}{{if .Blocked}}<div class="muted" style="font-size:11px;color:var(--warn)">{{.Blocked}}</div>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
<td><code style="font-size:11px">{{.Unit}}</code></td>
<td>{{if .Update}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('{{if .New}}Установить модуль {{.Available}}? Будет включена его служба.{{else}}Обновить компонент до {{.Available}}? Его служба перезапустится.{{end}}');"><input type="hidden" name="section" value="sec-update"><input type="hidden" name="component" value="{{.Name}}"><button type="submit" class="btn btn-ok">{{if .New}}Установить{{else}}Обновить{{end}}</button></form>{{end}}</td>
</tr>{{end}}
</tbody>
</table></div>
</details>
</div>
<div class="card">
<details class="adv-box">
<summary><span class="sum-h">Расширенные настройки</span><span class="muted">источник и канал обновлений, обновление из файла для сервера без выхода в интернет</span></summary>
<h3 class="adv-h">Обновление из файла</h3>
<p class="muted" style="margin:0 0 8px;font-size:12.5px"><b>Закрытый контур.</b> Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.</p>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<form method="post" action="/admin/setup/update/upload" enctype="multipart/form-data" style="margin:0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" onsubmit="return this.bundle.files.length>0 && confirm('Установить обновление из файла? Подпись пакета будет проверена; поставятся компоненты новее текущих — сначала ядро, затем модули. Службы перезапустятся.');">
<input type="hidden" name="section" value="sec-update">
<input type="file" name="bundle" accept=".zip,application/zip" required style="max-width:260px">
<button type="submit" class="btn btn-secondary" title="Пакет обновления с портала клиента — для сервера без выхода к серверу обновлений">Обновить из файла</button>
</form>
</div>
<h3 class="adv-h">Источник обновлений</h3>
<form method="post" action="/admin/setup/update" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<label>URL артефактории</label>
<input type="text" name="base_url" value="{{.Settings.Update.BaseURL}}" placeholder="https://lic.zetit.ru/updates (по умолчанию)">
<label>Канал</label>
<select name="channel">
<option value="stable" {{if eq .Settings.Update.Channel "stable"}}selected{{end}}>stable — стабильный</option>
<option value="beta" {{if eq .Settings.Update.Channel "beta"}}selected{{end}}>beta — предварительный</option>
</select>
<label>Публичный ключ издателя (base64 Ed25519)</label>
<input type="text" name="public_key" value="{{.Settings.Update.PublicKey}}" placeholder="зашит в релиз; переопределить здесь">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="auto_check" {{if .Settings.Update.AutoCheck}}checked{{end}} style="width:auto"> Проверять автоматически (раз в 6 часов)</label>
<div><button type="submit" class="btn">Сохранить</button></div>
</form>
</details>
</div>
{{if .InWizard}}</div>{{else}}</section>{{end}}
{{end}}
+3 -2
View File
@@ -14,6 +14,7 @@ var wizChapters = []struct {
Title string
Steps []int
}{
{"Обновление", []int{14}},
{"Сервер", []int{1, 2, 6, 12}},
{"Учёт и организация", []int{3, 4, 5}},
{"НРД", []int{7, 8}},
@@ -22,7 +23,7 @@ var wizChapters = []struct {
// wizStepTitles — короткие названия шагов (подзаголовки внутри главы).
var wizStepTitles = map[int]string{
1: "Лицензия", 2: "База bj", 6: "Валидата", 12: "Время и сеть",
1: "Лицензия", 2: "База bj", 6: "Валидата", 12: "Время и сеть", 14: "Обновление",
3: "Учёт DEPO", 4: "Организация", 5: "QUIK МНП",
7: "Шлюз НРД", 8: "Ключ подписи",
11: "Модули", 9: "Оповещения и копии", 13: "Учётные записи", 10: "Проверка",
@@ -111,7 +112,7 @@ func wizPath(rc *RuntimeConfig, done map[int]bool, current int) (path []int, cha
// wizChaptersLine — подзаголовок мастера: «Четыре главы: сервер, организация, НРД, запуск».
func wizChaptersLine(chapters []WizChapter) string {
words := []string{"", "Одна глава", "Две главы", "Три главы", "Четыре главы"}
words := []string{"", "Одна глава", "Две главы", "Три главы", "Четыре главы", "Пять глав"}
var names []string
for _, c := range chapters {
t := c.Title