package handlers import ( "io" "log" "net/http" "os" "sync" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/graph" "github.com/gin-gonic/gin" ) // GraphHandlers manages the service dependency graph. type GraphHandlers struct { path string mu sync.RWMutex yamlData []byte loaded *graph.Graph } // NewGraphHandlers loads the graph from the given YAML file path. func NewGraphHandlers(yamlPath string) *GraphHandlers { h := &GraphHandlers{path: yamlPath} if err := h.reload(); err != nil { if _, ok := err.(*os.PathError); ok { log.Printf("[graph] no graph file at %q, starting with empty graph", yamlPath) h.loaded = graph.New() h.yamlData = []byte("nodes: {}\n") } else { log.Fatalf("[graph] failed to load graph from %q: %v", yamlPath, err) } } return h } func (h *GraphHandlers) reload() error { data, err := os.ReadFile(h.path) if err != nil { return err } g, err := graph.ParseYAML(data) if err != nil { return err } h.mu.Lock() h.yamlData = data h.loaded = g h.mu.Unlock() return nil } // GetGraph returns the current parsed graph. func (h *GraphHandlers) GetGraph() *graph.Graph { h.mu.RLock() defer h.mu.RUnlock() return h.loaded } // GetYAML returns the raw YAML content. // @Summary Get dependency graph YAML // @Description Returns the service dependency graph as raw YAML text // @Tags graph // @Produce plain // @Success 200 {string} string "YAML content" // @Security Bearer // @Router /graph [get] func (h *GraphHandlers) GetYAML(c *gin.Context) { h.mu.RLock() defer h.mu.RUnlock() c.Data(http.StatusOK, "text/yaml", h.yamlData) } // UpdateYAML updates the graph from new YAML text. // @Summary Update dependency graph YAML // @Description Replaces the service dependency graph YAML and reloads it // @Tags graph // @Accept plain // @Produce json // @Param body body string true "New YAML content" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Security Bearer // @Router /graph [put] func (h *GraphHandlers) UpdateYAML(c *gin.Context) { body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read body"}) return } g, err := graph.ParseYAML(body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := os.WriteFile(h.path, body, 0o644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write graph file"}) return } h.mu.Lock() h.yamlData = body h.loaded = g h.mu.Unlock() log.Printf("[graph] updated graph from admin, saved to %s", h.path) c.JSON(http.StatusOK, gin.H{"message": "graph updated"}) } // StartupOrder returns the computed service startup order. // @Summary Get startup order // @Description Returns the topologically sorted service startup order // @Tags graph // @Produce json // @Success 200 {array} string // @Failure 400 {object} map[string]string // @Security Bearer // @Router /graph/order [get] func (h *GraphHandlers) StartupOrder(c *gin.Context) { h.mu.RLock() g := h.loaded h.mu.RUnlock() order, err := g.TopologicalSort() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, order) } // CycleCheck checks if the graph has cycles. // @Summary Check for cycles // @Description Returns whether the dependency graph contains cycles // @Tags graph // @Produce json // @Success 200 {object} map[string]bool // @Security Bearer // @Router /graph/cycle [get] func (h *GraphHandlers) CycleCheck(c *gin.Context) { h.mu.RLock() g := h.loaded h.mu.RUnlock() c.JSON(http.StatusOK, gin.H{"has_cycle": g.HasCycle()}) }