package lkgateway // wizdefer.go — «Настроить позже» в мастере: необязательные шаги (модули, оповещения, // учётные записи) и отдельные модули можно отложить — мастер считает их завершёнными // и не держит главу «Запуск» незакрытой. Как только настроено по-настоящему, отметка // не нужна: готовность всё равно считается из настроек. Отметку можно снять. import ( "context" "encoding/json" "net/http" "os" "strconv" "strings" "sync" ) const wizDeferredFile = "/var/lib/bj/.bj/wizard-deferred.json" // wizDeferrable — шаги, которые можно отложить (необязательные для обмена с НРД). var wizDeferrable = map[int]bool{9: true, 11: true, 13: true} type wizDeferred struct { Steps map[int]bool `json:"steps,omitempty"` Modules map[string]bool `json:"modules,omitempty"` } var wizDeferredMu sync.Mutex func loadWizDeferred() wizDeferred { wizDeferredMu.Lock() defer wizDeferredMu.Unlock() return readWizDeferred() } func readWizDeferred() wizDeferred { var d wizDeferred if b, err := os.ReadFile(wizDeferredFile); err == nil { _ = json.Unmarshal(b, &d) } if d.Steps == nil { d.Steps = map[int]bool{} } if d.Modules == nil { d.Modules = map[string]bool{} } return d } func updateWizDeferred(mut func(*wizDeferred)) error { wizDeferredMu.Lock() defer wizDeferredMu.Unlock() d := readWizDeferred() mut(&d) b, err := json.MarshalIndent(&d, "", " ") if err != nil { return err } tmp := wizDeferredFile + ".tmp" if err := os.WriteFile(tmp, b, 0o640); err != nil { return err } return os.Rename(tmp, wizDeferredFile) } // registerWizDefer — POST /admin/setup/wizard-defer: step=N [module=key] [undo=1]. func registerWizDefer(mux *http.ServeMux) { mux.HandleFunc("/admin/setup/wizard-defer", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method", http.StatusMethodNotAllowed) return } _ = r.ParseForm() step, _ := strconv.Atoi(r.FormValue("step")) module := strings.TrimSpace(r.FormValue("module")) undo := r.FormValue("undo") == "1" if !wizDeferrable[step] { http.Error(w, "этот шаг нельзя отложить", http.StatusBadRequest) return } if module != "" && !moduleKnown(module) { http.Error(w, "неизвестный модуль", http.StatusBadRequest) return } _ = updateWizDeferred(func(d *wizDeferred) { switch { case module != "" && undo: delete(d.Modules, module) case module != "": d.Modules[module] = true case undo: delete(d.Steps, step) default: d.Steps[step] = true } }) http.Redirect(w, r, "/admin/wizard?step="+strconv.Itoa(step), http.StatusSeeOther) }) } func moduleKnown(key string) bool { for _, m := range wizModules { if m == key { return true } } return false } // wizDone — выполнены ли шаги мастера. Одно правило для мастера и плашки «Требуется // настройка» на главной: тот же путь (лицензия, автоматика) и те же отметки «отложено». // validata — версия «Валидаты» ("" — нет), certsDone — ключ организации подключён. func wizDone(ctx context.Context, rc *RuntimeConfig, validata string, certsDone bool) map[int]bool { s := rc.Snapshot() def := loadWizDeferred() mnp := moduleAllowed(rc, "quik-mnp") srv, _ := timeStatus(ctx) _, upd := updateComponentRows(rc) mods := true for _, m := range wizModuleViews(rc, s) { if !m.Ready && !def.Modules[m.Key] { mods = false } } return map[int]bool{ 1: licenseStatus(rc).Valid, 2: s.Postgres.DSN != "", 3: !mnp || (strings.TrimSpace(s.Fancy.Server) != "" && s.Fancy.DepoTablesCheckOK && s.Fancy.DepoJobCheckOK), 4: doneOrg(s), 5: !mnp || doneQuik(s), 6: validata != "" || (s.Crypto.Provider != "" && s.Crypto.Provider != "stub" && s.Crypto.ModulePath != ""), 7: doneNSD(s), 8: certsDone, 9: doneAlerts(s) || def.Steps[9], 10: s.LastTest != nil || (!devMode(rc) && readinessDone()), 11: mods || def.Steps[11], 12: srv && !s.WorkHours.SavedAt.IsZero(), 13: doneUsers() || def.Steps[13], 14: !upd, } }