package main import ( "encoding/json" "sync" "time" ) // WizardStage — какой странице wizard'а соответствует текущее состояние. // Переходы: welcome → precheck → config → installing → done. // Из любого можно вернуться в welcome (полный reset). type WizardStage string const ( StageWelcome WizardStage = "welcome" StagePrecheck WizardStage = "precheck" StageConfig WizardStage = "config" StageInstalling WizardStage = "installing" StageDone WizardStage = "done" StageError WizardStage = "error" ) // Config — данные, которые wizard собирает на стадии config. type Config struct { OrgINN string `json:"orgInn"` // ИНН организации OrgName string `json:"orgName"` // отображаемое имя AdminEmail string `json:"adminEmail"` // куда писать алерты LicenseKey string `json:"licenseKey"` // годовой ключ (опционально, можно пропустить) } // StepStatus — текущее состояние конкретного шага установки. type StepStatus string const ( StepPending StepStatus = "pending" StepRunning StepStatus = "running" StepDone StepStatus = "done" StepSkipped StepStatus = "skipped" StepFailed StepStatus = "failed" ) // StepState — снимок одного шага для отдачи в UI. type StepState struct { ID string `json:"id"` Title string `json:"title"` Status StepStatus `json:"status"` Message string `json:"message,omitempty"` Started *time.Time `json:"started,omitempty"` Finished *time.Time `json:"finished,omitempty"` } // PrecheckResult — результат одной системной проверки на стадии precheck. type PrecheckResult struct { ID string `json:"id"` Title string `json:"title"` OK bool `json:"ok"` Message string `json:"message,omitempty"` } // State — потокобезопасное состояние wizard'а. Хранит всё что нужно // отрисовать на любой из страниц + текущий прогресс установки. type State struct { mu sync.RWMutex artifactsDir string Stage WizardStage `json:"stage"` ErrorMsg string `json:"errorMsg,omitempty"` Precheck []PrecheckResult `json:"precheck"` Config Config `json:"config"` Steps []StepState `json:"steps"` bus *eventBus } func newState(artifactsDir string) *State { return &State{ artifactsDir: artifactsDir, Stage: StageWelcome, Steps: buildStepList(), bus: newEventBus(), } } // Snapshot — потокобезопасная копия для GET /api/state. func (s *State) Snapshot() State { s.mu.RLock() defer s.mu.RUnlock() cp := *s cp.Precheck = append([]PrecheckResult(nil), s.Precheck...) cp.Steps = append([]StepState(nil), s.Steps...) return cp } func (s *State) setStage(st WizardStage) { s.mu.Lock() s.Stage = st s.mu.Unlock() s.bus.publish(event{Type: "stage", Data: mustJSON(map[string]string{"stage": string(st)})}) } func (s *State) setError(msg string) { s.mu.Lock() s.Stage = StageError s.ErrorMsg = msg s.mu.Unlock() s.bus.publish(event{Type: "error", Data: mustJSON(map[string]string{"message": msg})}) } func (s *State) setPrecheck(items []PrecheckResult) { s.mu.Lock() s.Precheck = items s.mu.Unlock() s.bus.publish(event{Type: "precheck", Data: mustJSON(items)}) } func (s *State) setConfig(c Config) { s.mu.Lock() s.Config = c s.mu.Unlock() } func (s *State) updateStep(id string, fn func(*StepState)) { s.mu.Lock() var snap StepState for i := range s.Steps { if s.Steps[i].ID == id { fn(&s.Steps[i]) snap = s.Steps[i] break } } s.mu.Unlock() s.bus.publish(event{Type: "step", Data: mustJSON(snap)}) } func mustJSON(v any) string { b, _ := json.Marshal(v) return string(b) }