package api import ( "encoding/json" "errors" "fmt" "net/http" "strconv" "time" "github.com/mrleo1nid/pzmanager/internal/pzserver" ) func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "server": s.sup.Status(), "task": s.jobs.Status(), }) } func (s *Server) handleStart(w http.ResponseWriter, r *http.Request) { if err := s.sup.Start(); err != nil { writeOpError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"state": s.sup.State()}) } // handleStop останавливает сервер в фоне: корректное завершение занимает до // нескольких минут, и держать HTTP-запрос всё это время незачем — панель // видит прогресс по состоянию и логам. func (s *Server) handleStop(w http.ResponseWriter, r *http.Request) { if s.sup.State() == pzserver.StateStopped { writeOpError(w, pzserver.ErrNotRunning) return } go func() { if err := s.sup.Stop(); err != nil && !errors.Is(err, pzserver.ErrNotRunning) { s.sup.AppendLog("manager", "Остановка не удалась: "+err.Error()) } }() writeJSON(w, http.StatusAccepted, map[string]any{"state": pzserver.StateStopping}) } func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) { if !s.sup.Installed() { writeOpError(w, pzserver.ErrNotInstalled) return } go func() { if err := s.sup.Restart(); err != nil { s.sup.AppendLog("manager", "Перезапуск не удался: "+err.Error()) } }() writeJSON(w, http.StatusAccepted, map[string]any{"state": s.sup.State()}) } func (s *Server) handleConsole(w http.ResponseWriter, r *http.Request) { var body struct { Command string `json:"command"` } if !decodeJSON(w, r, &body) { return } if err := s.sup.Send(body.Command); err != nil { writeOpError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"sent": body.Command}) } // handleLogs отдаёт историю консоли начиная с указанного номера строки. func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) { since, _ := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64) writeJSON(w, http.StatusOK, map[string]any{"lines": s.sup.Lines(since)}) } // handleLogStream — живая консоль через Server-Sent Events. SSE выбран вместо // вебсокетов: поток односторонний, а переподключение браузер берёт на себя. func (s *Server) handleLogStream(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "потоковая отдача не поддерживается") return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") // Отключаем буферизацию в nginx, иначе строки придут пачкой. w.Header().Set("X-Accel-Buffering", "no") w.WriteHeader(http.StatusOK) lines, unsubscribe := s.sup.Subscribe() defer unsubscribe() // Сначала отдаём хвост истории, чтобы вкладка не открывалась пустой. since, _ := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64) for _, line := range s.sup.Lines(since) { if !writeSSE(w, line) { return } } flusher.Flush() // Пинг не даёт прокси закрыть простаивающее соединение. ping := time.NewTicker(25 * time.Second) defer ping.Stop() for { select { case <-r.Context().Done(): return case line, ok := <-lines: if !ok { return } if !writeSSE(w, line) { return } flusher.Flush() case <-ping.C: if _, err := fmt.Fprint(w, ": ping\n\n"); err != nil { return } flusher.Flush() } } } func writeSSE(w http.ResponseWriter, line pzserver.LogLine) bool { payload, err := json.Marshal(line) if err != nil { return true } _, err = fmt.Fprintf(w, "data: %s\n\n", payload) return err == nil }