From 775b4214b63195c4ee14d97e213447304a51587b Mon Sep 17 00:00:00 2001 From: zuevav Date: Thu, 24 Sep 2026 22:04:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=20=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=D0=BE=D0=BC=20=D1=85=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=20(=D1=8D=D1=82=D0=B0=D0=BF=D1=8B,=20=D1=81=D0=BA=D0=B0=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=9C=D0=91,=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20=D1=8F=D0=B4?= =?UTF-8?q?=D1=80=D0=B0);=20=D0=B2=20=D0=BC=D0=B0=D1=81=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=20=D0=BA=D0=BB=D1=8E=D1=87=20=D0=BF=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D1=83=D0=BA=D1=82=D0=B0=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=B8=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /admin/setup/update/progress: этап, байты, целевая версия ядра; ApplyUpdate/ApplyBundle отчитываются; - окно в layout: формы установки обновлений отправляются скриптом, окно ждёт перезапуска ядра и сверяет версию; - шаг 1 мастера: «Ключ продукта введён», смена ключа — под ссылкой. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi --- internal/lkgateway/setup.go | 1 + internal/lkgateway/updater.go | 42 +++++++ internal/lkgateway/updprogress.go | 109 ++++++++++++++++++ .../lkgateway/web/templates/admin_wizard.html | 10 +- internal/lkgateway/web/templates/layout.html | 74 ++++++++++++ 5 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 internal/lkgateway/updprogress.go diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 4209fd9..29834dc 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -79,6 +79,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service mux.HandleFunc("/admin/setup/update/check", h.checkUpdate) mux.HandleFunc("/admin/setup/update/apply", h.applyUpdate) mux.HandleFunc("/admin/setup/update/upload", h.uploadUpdate) + mux.HandleFunc("/admin/setup/update/progress", h.updateProgress) mux.HandleFunc("/admin/setup/license", h.saveLicense) mux.HandleFunc("/admin/setup/license/activate", h.licenseActivate) mux.HandleFunc("/admin/setup/license/renew", h.licenseRenewNow) diff --git a/internal/lkgateway/updater.go b/internal/lkgateway/updater.go index b780ab2..d98bbfc 100644 --- a/internal/lkgateway/updater.go +++ b/internal/lkgateway/updater.go @@ -147,6 +147,14 @@ func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) { // ApplyUpdate — скачать выбранные компоненты (names пусто — все, для которых есть обновление) и // передать установку root-установщику. Если среди них ядро, bj-server перезапустится до ответа. func (u *Updater) ApplyUpdate(ctx context.Context, names []string) (string, error) { + progressBegin(4) + progressStage(1, "Проверка выпуска на сервере лицензий") + msg, err := u.applyUpdate(ctx, names) + progressEnd(msg, err) + return msg, err +} + +func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, error) { if licensingEnabled(u.rc) { ls := licenseStatus(u.rc) if !ls.Valid { @@ -221,10 +229,24 @@ func (u *Updater) ApplyUpdate(ctx context.Context, names []string) (string, erro Names []string `json:"names"` New []string `json:"new,omitempty"` } + var total int64 + for _, c := range selected { + for _, a := range m.Artifacts { + if a.Name == c.Name { + total += a.Size + } + } + if c.Name == "bj-server" { + progressTarget(c.Available) + } + } + progressStage(2, "Скачивание файлов") + stopWatch := progressWatchDir(staging, total) for _, c := range selected { for _, a := range m.Artifacts { if a.Name == c.Name { if _, err := cl.DownloadArtifact(ctx, a, staging); err != nil { + stopWatch() return "", fmt.Errorf("скачивание %s: %w", c.Name, err) } } @@ -235,10 +257,12 @@ func (u *Updater) ApplyUpdate(ctx context.Context, names []string) (string, erro } list = append(list, c.Name+" "+c.Available) } + stopWatch() rb, _ := json.Marshal(req) if err := os.WriteFile(filepath.Join(staging, "request.json"), rb, 0o640); err != nil { return "", err } + progressStage(3, "Проверка подписи и установка, затем перезапуск служб") log.Printf("lk-gateway: обновление компонентов: %s — передано root-установщику", strings.Join(list, ", ")) wctx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() @@ -406,6 +430,14 @@ func (u *Updater) updateLoop(ctx context.Context) { // модули за ним. Модуль, которому нужно более новое ядро, чем будет после установки, // пропускается с объяснением. func (u *Updater) ApplyBundle(ctx context.Context, zipPath string) (string, error) { + progressBegin(4) + progressStage(1, "Распаковка и проверка пакета") + msg, err := u.applyBundle(ctx, zipPath) + progressEnd(msg, err) + return msg, err +} + +func (u *Updater) applyBundle(ctx context.Context, zipPath string) (string, error) { defer os.Remove(zipPath) if licensingEnabled(u.rc) { ls := licenseStatus(u.rc) @@ -481,6 +513,16 @@ func (u *Updater) ApplyBundle(ctx context.Context, zipPath string) (string, erro if err := os.WriteFile(filepath.Join(staging, "request.json"), rb, 0o640); err != nil { return "", err } + for _, n := range req.Names { + if n == "bj-server" { + for _, c := range plan { + if c.Name == n { + progressTarget(c.Available) + } + } + } + } + progressStage(3, "Проверка подписи и установка, затем перезапуск служб") log.Printf("lk-gateway: обновление из файла (релиз %s): %s — передано root-установщику", m.Version, strings.Join(list, ", ")) wctx, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() diff --git a/internal/lkgateway/updprogress.go b/internal/lkgateway/updprogress.go new file mode 100644 index 0000000..b0d6a5c --- /dev/null +++ b/internal/lkgateway/updprogress.go @@ -0,0 +1,109 @@ +package lkgateway + +// Ход обновления для окна с индикатором на странице: этапы (проверка, скачивание, установка, +// перезапуск) и байты скачанного. Страница опрашивает GET /admin/setup/update/progress; когда +// обновляется ядро, bj-server перезапускается — страница видит обрыв связи, ждёт и сверяет +// версию работающего ядра с целевой. + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "sync" + "time" +) + +type updateProgressState struct { + Seq int64 `json:"seq"` + Active bool `json:"active"` + Finished bool `json:"finished"` + Step int `json:"step"` + Steps int `json:"steps"` + Stage string `json:"stage"` + Done int64 `json:"done"` + Total int64 `json:"total"` + Target string `json:"target,omitempty"` // версия ядра после обновления (если ядро обновляется) + Result string `json:"result,omitempty"` + Err string `json:"error,omitempty"` + Started time.Time `json:"started"` + Version string `json:"version"` // версия работающего bj-server +} + +var updProg = struct { + sync.Mutex + s updateProgressState +}{} + +func progressBegin(steps int) { + updProg.Lock() + defer updProg.Unlock() + updProg.s = updateProgressState{Seq: updProg.s.Seq + 1, Active: true, Steps: steps, Started: time.Now()} +} + +func progressStage(step int, stage string) { + updProg.Lock() + defer updProg.Unlock() + updProg.s.Step, updProg.s.Stage = step, stage +} + +func progressBytes(done, total int64) { + updProg.Lock() + defer updProg.Unlock() + updProg.s.Done, updProg.s.Total = done, total +} + +func progressTarget(v string) { + updProg.Lock() + defer updProg.Unlock() + updProg.s.Target = v +} + +func progressEnd(result string, err error) { + updProg.Lock() + defer updProg.Unlock() + updProg.s.Active, updProg.s.Finished, updProg.s.Result = false, true, result + if err != nil { + updProg.s.Err = err.Error() + } +} + +// progressWatchDir — пока идёт скачивание, раз в полсекунды считает байты в каталоге подготовки. +// Возвращает функцию остановки. +func progressWatchDir(dir string, total int64) func() { + stop := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + t := time.NewTicker(500 * time.Millisecond) + defer t.Stop() + for { + var n int64 + _ = filepath.Walk(dir, func(_ string, fi os.FileInfo, err error) error { + if err == nil && fi.Mode().IsRegular() { + n += fi.Size() + } + return nil + }) + progressBytes(n, total) + select { + case <-stop: + return + case <-t.C: + } + } + }() + return func() { close(stop); wg.Wait(); progressBytes(total, total) } +} + +// updateProgressHandler — GET /admin/setup/update/progress. +func (h *setupHandlers) updateProgress(w http.ResponseWriter, r *http.Request) { + updProg.Lock() + s := updProg.s + updProg.Unlock() + s.Version = BuildVersion + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + _ = json.NewEncoder(w).Encode(s) +} diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index db80274..1f7662c 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -50,7 +50,7 @@
Что это. Ключ продукта (BJ-XXXX-XXXX-XXXX) привязывает этот сервер к вашей лицензии.
Зачем. От лицензии зависят модули (обмен с DEPO и QUIK, личный кабинет, НПР/УДС) и получение обновлений: bj скачает и поставит модули, входящие в лицензию, автоматически.
Где взять. Портал клиента lic.zetit.ru/portal или письмо от ZETIT.
- {{if .Done.License}}

✓ Лицензия активна: {{.Lic.Tenant}}, план {{.Lic.Plan}}, модули: {{if .Lic.Features}}{{range $i, $f := .Lic.Features}}{{if $i}}, {{end}}{{$f}}{{end}}{{else}}только ядро{{end}}; {{.Lic.Message}}

{{end}} + {{if .Done.License}}

✓ Ключ продукта введён, лицензия активна: {{.Lic.Tenant}}, план {{.Lic.Plan}}, модули: {{if .Lic.Features}}{{range $i, $f := .Lic.Features}}{{if $i}}, {{end}}{{$f}}{{end}}{{else}}только ядро{{end}}; {{.Lic.Message}}

{{end}} {{with .Update}}{{if .HasUpdate}}

На сервере лицензий есть что поставить (выпуск {{.Available}})

@@ -61,12 +61,20 @@
{{else}}

✓ Установлена последняя версия bj ({{.Available}}).

{{end}}{{end}} + {{if .Done.License}}
Сменить ключ продукта (обычно не нужно)
+
{{else}} +
+ + + + +
{{end}}
{{if .Done.License}}К шагу 2 →{{else}}Пропустить (ввести позже) →{{end}}
diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index f76f417..e2bc3f6 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -572,6 +572,80 @@ label { font-size:13px; font-weight:500; } })(); })(); + + {{end}}