From 560c39da18cb42e1cedc1ea1e28c3d1bc4b02f27 Mon Sep 17 00:00:00 2001 From: zuevav Date: Fri, 18 Sep 2026 19:39:09 +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=D1=8F:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20?= =?UTF-8?q?=C2=AB=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=C2=BB=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D0=BE=D1=8F=D1=89=D0=B8=D0=B9=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82,=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B8=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BD=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=BB=D0=B0=D0=B3=D0=B0=D1=8E=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Жалоба со стенда: в «Обновлениях» висит ЛК 0.2.0, нажимаешь «Обновить» — ничего не происходит. На деле обновление уходило в фон и падало («манифест HTTP 404: релизов в канале нет» — стенд на канале stable, релизы выкладываются в beta), а отказ оседал в незаметной строке «Последняя проверка». - Модули обновляются на месте и во flash показывается фактический результат; в фон уходит только обновление ядра, где ответа установщика всё равно не дождаться (bj-server перезапускается раньше). - Строка результата при отказе показывается предупреждением, а не серым текстом. - Если последняя проверка не удалась, кнопки установки скрыты, у версии — пояснение «нажмите „Проверить обновления“»: предлагать установку по данным неудачной проверки нельзя. - Версии запоминаются вместе с каналом (components_channel); при смене канала прошлые данные забываются — они относились к другому каналу. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL --- internal/lkgateway/runtimeconfig.go | 4 ++ internal/lkgateway/setup.go | 37 ++++++++--- internal/lkgateway/updater.go | 56 ++++++++++++++++- internal/lkgateway/updater_rows_test.go | 62 +++++++++++++++++++ .../lkgateway/web/templates/admin_setup.html | 2 +- 5 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 internal/lkgateway/updater_rows_test.go diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go index fdc225e..bff4358 100644 --- a/internal/lkgateway/runtimeconfig.go +++ b/internal/lkgateway/runtimeconfig.go @@ -96,6 +96,10 @@ type UpdateSettings struct { Notes string `json:"notes,omitempty"` // заметки доступного релиза // Components — версии компонентов в последнем проверенном релизе (имя → версия). Components map[string]string `json:"components,omitempty"` + // ComponentsChannel — канал, в котором эти версии были найдены. Если канал + // сменили, данные прошлой проверки к новому каналу не относятся: предлагать + // по ним обновление нельзя (кнопка «Обновить» уходила бы в пустоту). + ComponentsChannel string `json:"components_channel,omitempty"` } // NewsSettings — лента новостей (события системы, окна техработ, обновления diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 87122fb..30a2665 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -326,6 +326,7 @@ type SetupData struct { LKPreview LKPreviewStatus // кабинет на этом сервере для просмотра LKData LKDataView // курсы валют и ежедневный срез стоимости Units []UnitView // службы bj и её модулей + UpdateFailed bool // последняя попытка обновления закончилась отказом Versions SystemVersions // версии компонентов bj и внешних систем Modules map[string]bool // какие лицензируемые модули разрешены (ключ = признак) Features []LicenseFeatureView // признаки лицензии для карточки @@ -394,6 +395,7 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, req *http.Request, fl LKPreview: lkPreviewStatus(h.rc, req), LKData: lkDataView(h.rc), Units: unitViews(h.rc), + UpdateFailed: updateResultFailed(s.Update.LastResult), LKKeyMask: lkKeyForPage(s.LK.APIKey), Versions: systemVersions(h.rc), Modules: modulesAllowed(h.rc), @@ -1243,10 +1245,16 @@ func (h *setupHandlers) saveUpdate(w http.ResponseWriter, r *http.Request) { return } cfg := h.rc.Snapshot().Update + was := updateChannel(cfg) cfg.BaseURL = strings.TrimSpace(r.FormValue("base_url")) cfg.Channel = strings.TrimSpace(r.FormValue("channel")) cfg.PublicKey = strings.TrimSpace(r.FormValue("public_key")) cfg.AutoCheck = r.FormValue("auto_check") != "" + if updateChannel(cfg) != was { + // Версии из прошлого канала к новому не относятся — забываем их до проверки. + cfg.Components, cfg.ComponentsChannel, cfg.Available, cfg.Notes = nil, "", "", "" + cfg.LastResult = "канал сменён на «" + updateChannel(cfg) + "» — нажмите «Проверить обновления»" + } if err := h.rc.SaveUpdateSettings(cfg); err != nil { setupFlash(w, r, "обновления: ошибка сохранения: "+err.Error()) return @@ -1277,24 +1285,35 @@ func (h *setupHandlers) applyUpdate(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() names := r.Form["component"] rc := h.rc - // В фоне: при обновлении ядра bj-server перезапустится раньше, чем придёт ответ установщика. - go func() { - msg, err := NewUpdater(rc).ApplyUpdate(context.Background(), names) - cfg := rc.Snapshot().Update + save := func(msg string, err error) string { if err != nil { msg = "Обновление не применено: " + err.Error() } else { msg = "Обновление: " + msg } log.Printf("lk-gateway: %s", msg) + cfg := rc.Snapshot().Update cfg.LastResult, cfg.LastCheck = msg, time.Now().UTC() _ = rc.SaveUpdateSettings(cfg) - }() - what := "всех компонентов с новой версией" - if len(names) > 0 { - what = strings.Join(names, ", ") + return msg } - setupFlash(w, r, "Обновление "+what+" запущено: файлы скачиваются и проверяются, затем перезапускаются службы. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.") + // Обновление ядра — только в фоне: bj-server перезапустится раньше, чем придёт + // ответ установщика, и страница всё равно не дождётся. Модули обновляем на месте + // и показываем настоящий результат: иначе отказ («в канале нет релизов», нет + // лицензии, не сходится подпись) виден только строкой «Последняя проверка», + // и со стороны выглядит так, будто кнопка ничего не делает. + if updateTouchesCore(rc, names) { + go func() { save(NewUpdater(rc).ApplyUpdate(context.Background(), names)) }() + what := "всех компонентов с новой версией" + if len(names) > 0 { + what = strings.Join(names, ", ") + } + setupFlash(w, r, "Обновление "+what+" запущено: файлы скачиваются и проверяются, затем перезапускаются службы. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.") + return + } + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute) + defer cancel() + setupFlash(w, r, save(NewUpdater(rc).ApplyUpdate(ctx, names))) } // uploadUpdate — POST /admin/setup/update/upload: пакет обновления файлом (закрытый контур). diff --git a/internal/lkgateway/updater.go b/internal/lkgateway/updater.go index 8b7bd0e..2774cb5 100644 --- a/internal/lkgateway/updater.go +++ b/internal/lkgateway/updater.go @@ -119,6 +119,16 @@ func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateStatus, error) { return st, nil } +// updateChannel — канал обновлений установки (пусто = stable). +func updateChannel(s UpdateSettings) string { + if c := strings.TrimSpace(s.Channel); c != "" { + return c + } + return "stable" +} + +func (u *Updater) channel() string { return updateChannel(u.rc.Snapshot().Update) } + func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) { cfg := u.rc.Snapshot().Update cfg.LastCheck = time.Now().UTC() @@ -127,6 +137,7 @@ func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) { cfg.Notes = st.Notes if avail != nil { cfg.Components = avail + cfg.ComponentsChannel = u.channel() } if err := u.rc.SaveUpdateSettings(cfg); err != nil { log.Printf("lk-gateway: сохранение результата проверки обновления: %v", err) @@ -284,7 +295,13 @@ func (u *Updater) ensureSplitModules(ctx context.Context) { // установленное на этом сервере и версии из последней проверки. func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) { rows := release.Plan("", nil, release.LoadInstalled(release.InstalledFile), BuildVersion) - avail := rc.Snapshot().Update.Components + set := rc.Snapshot().Update + avail := set.Components + // Версии из проверки в другом канале к текущему каналу не относятся: по ним + // «Обновить» уходило бы в пустоту («релизов в канале нет»). + if set.ComponentsChannel != "" && set.ComponentsChannel != updateChannel(set) { + avail = nil + } var out []release.ComponentState any := false vers := systemVersions(rc) @@ -299,6 +316,16 @@ func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) { } } release.MarkSplitModules(out, func(f string) bool { return moduleAllowed(rc, f) }) + // Последняя проверка не удалась — предлагать установку по старым данным нельзя: + // кнопка ушла бы в тот же отказ. Показываем причину и просим проверить заново. + if updateResultFailed(set.LastResult) { + for i := range out { + if out[i].Update && out[i].Available != "" { + out[i].Blocked = "проверка обновлений не удалась — нажмите «Проверить обновления»" + } + out[i].Update = false + } + } for _, r := range out { any = any || r.Update } @@ -460,3 +487,30 @@ func (u *Updater) ApplyBundle(ctx context.Context, zipPath string) (string, erro } return msg, nil } + +// updateTouchesCore — затронет ли обновление ядро (тогда ответа установщика не дождаться). +func updateTouchesCore(rc *RuntimeConfig, names []string) bool { + if len(names) == 0 { + // «Обновить всё»: ядро попадёт в набор, если для него есть новая версия + rows, _ := updateComponentRows(rc) + for _, r := range rows { + if r.Core && r.Update { + return true + } + } + return false + } + for _, n := range names { + if c, ok := release.ComponentByName(n); ok && c.Core { + return true + } + } + return false +} + +// updateResultFailed — последняя строка результата говорит об отказе. +func updateResultFailed(result string) bool { + r := strings.ToLower(result) + return strings.Contains(r, "не применено") || strings.Contains(r, "ошибка") || + strings.Contains(r, "не удалось") || strings.Contains(r, "недоступ") +} diff --git a/internal/lkgateway/updater_rows_test.go b/internal/lkgateway/updater_rows_test.go new file mode 100644 index 0000000..8ec04e8 --- /dev/null +++ b/internal/lkgateway/updater_rows_test.go @@ -0,0 +1,62 @@ +package lkgateway + +import ( + "path/filepath" + "testing" +) + +// После неудачной проверки нельзя предлагать установку по старым данным: кнопка +// «Обновить» уходила бы в тот же отказ, и со стороны это выглядит как «ничего не +// происходит» (жалоба 18.09 по стенду: ЛК 0.2.0, канал stable пуст). +func TestUpdateRowsAfterFailedCheck(t *testing.T) { + rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json")) + if err != nil { + t.Fatal(err) + } + cfg := rc.Snapshot().Update + cfg.Channel = "stable" + cfg.Components = map[string]string{"bj-lk": "0.2.0"} + cfg.ComponentsChannel = "stable" + cfg.LastResult = "Обновление не применено: манифест: release: манифест HTTP 404: релизов в канале нет" + if err := rc.SaveUpdateSettings(cfg); err != nil { + t.Fatal(err) + } + rows, any := updateComponentRows(rc) + if any { + t.Error("после отказа проверки предлагается обновление") + } + for _, r := range rows { + if r.Update { + t.Errorf("%s: кнопка обновления осталась", r.Name) + } + if r.Name == "bj-lk" && r.Available == "0.2.0" && r.Blocked == "" { + t.Error("не объяснено, почему обновление недоступно") + } + } + + // Версии, найденные в другом канале, к текущему не относятся. + cfg.LastResult = "доступны обновления: bj-lk 0.2.0" + cfg.ComponentsChannel = "beta" + if err := rc.SaveUpdateSettings(cfg); err != nil { + t.Fatal(err) + } + for _, r := range updateComponentRowsOnly(rc) { + if r.Available != "" { + t.Errorf("%s: показана версия из чужого канала (%s)", r.Name, r.Available) + } + } + if !updateResultFailed("Обновление не применено: нет лицензии") || updateResultFailed("все компоненты актуальны") { + t.Error("определение отказа работает неверно") + } +} + +func updateComponentRowsOnly(rc *RuntimeConfig) []componentRow { + rows, _ := updateComponentRows(rc) + out := make([]componentRow, 0, len(rows)) + for _, r := range rows { + out = append(out, componentRow{Name: r.Name, Available: r.Available}) + } + return out +} + +type componentRow struct{ Name, Available string } diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index f533ca1..eb5ee5e 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -1115,7 +1115,7 @@ function hbBind(inp,box,list,onPick){ {{if .Settings.Update.Notes}}

Что нового в релизе {{.Settings.Update.Available}}: {{.Settings.Update.Notes}}

{{end}}

Закрытый контур. Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.

- {{if not .Settings.Update.LastCheck.IsZero}}

Последняя проверка: {{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}}. {{.Settings.Update.LastResult}}

{{end}} + {{if not .Settings.Update.LastCheck.IsZero}}{{if .UpdateFailed}}
{{.Settings.Update.LastResult}} ({{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}})
{{else}}

Последняя проверка: {{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}}. {{.Settings.Update.LastResult}}

{{end}}{{end}} {{if and .License.Present (not .License.AllowsUpdates)}}

Текущий план «{{.License.Plan}}» не включает обновления.

{{end}}