package dag import ( "context" "fmt" "net/http" "os" "strings" "github.com/gofiber/fiber/v2" "github.com/oarkflow/form" "github.com/oarkflow/json/jsonparser" "github.com/oarkflow/mq" "github.com/oarkflow/mq/consts" ) // RenderNotFound handles 404 errors. func renderFiberNotFound(c *fiber.Ctx) error { html := `

task not found

Back to home

` c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.Status(fiber.StatusNotFound).SendString(html) } // Render handles process and request routes. func (tm *DAG) RenderFiber(c *fiber.Ctx) error { ctx, data, err := form.ParseBodyAsJSON(c.UserContext(), c.Get("Content-Type"), c.Body(), c.Queries()) if err != nil { return c.Status(fiber.StatusNotFound).SendString(err.Error()) } accept := c.Get("Accept") userCtx := form.UserContext(ctx) ctx = context.WithValue(ctx, "method", c.Method()) if c.Method() == fiber.MethodGet && userCtx.Get("task_id") != "" { manager, ok := tm.taskManager.Get(userCtx.Get("task_id")) if !ok || manager == nil { if strings.Contains(accept, fiber.MIMETextHTML) || accept == "" { return renderFiberNotFound(c) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "task not found"}) } } result := tm.Process(ctx, data) if result.Error != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": result.Error.Error()}) } if result.Ctx == nil { result.Ctx = ctx } contentType := consts.TypeJson if ct, ok := result.Ctx.Value(consts.ContentType).(string); ok { contentType = ct } switch contentType { case consts.TypeHtml: htmlContent, err := jsonparser.GetString(result.Payload, "html_content") if err != nil { return err } if strings.Contains(htmlContent, "{{current_uri}}") { htmlContent = strings.ReplaceAll(htmlContent, "{{current_uri}}", c.Path()) } c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.SendString(htmlContent) default: if c.Method() != fiber.MethodPost { return c.Status(fiber.StatusMethodNotAllowed).JSON(fiber.Map{"message": "not allowed"}) } c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) return c.JSON(result.Payload) } } // TaskStatusHandler retrieves task statuses. func (tm *DAG) fiberTaskStatusHandler(c *fiber.Ctx) error { taskID := c.Query("taskID") if taskID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": "taskID is missing"}) } manager, ok := tm.taskManager.Get(taskID) if !ok { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"message": "Invalid TaskID"}) } result := make(map[string]TaskState) manager.taskStates.ForEach(func(key string, value *TaskState) bool { key = strings.Split(key, Delimiter)[0] nodeID := strings.Split(value.NodeID, Delimiter)[0] rs := jsonparser.Delete(value.Result.Payload, "html_content") status := value.Status if status == mq.Processing { status = mq.Completed } state := TaskState{ NodeID: nodeID, Status: status, UpdatedAt: value.UpdatedAt, Result: mq.Result{ Payload: rs, Error: value.Result.Error, Status: status, }, } result[key] = state return true }) return c.Type(fiber.MIMEApplicationJSON).JSON(result) } func (tm *DAG) BaseURI() string { return tm.httpPrefix } // processSVGContent processes SVG to ensure proper scaling using viewBox func (tm *DAG) processSVGContent(svgContent string) string { // Extract width and height from SVG var widthVal, heightVal float64 var hasWidth, hasHeight bool // Find width attribute if strings.Contains(svgContent, `width="`) { start := strings.Index(svgContent, `width="`) + 7 end := strings.Index(svgContent[start:], `"`) if end > 0 { width := svgContent[start : start+end] // Remove pt, px, or other units and convert to float cleanWidth := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(width, "pt"), "px"), "in") if _, err := fmt.Sscanf(cleanWidth, "%f", &widthVal); err == nil { hasWidth = true // Convert pt to pixels (1pt = 1.33px approximately) if strings.HasSuffix(width, "pt") { widthVal *= 1.33 } } } } // Find height attribute if strings.Contains(svgContent, `height="`) { start := strings.Index(svgContent, `height="`) + 8 end := strings.Index(svgContent[start:], `"`) if end > 0 { height := svgContent[start : start+end] // Remove pt, px, or other units and convert to float cleanHeight := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(height, "pt"), "px"), "in") if _, err := fmt.Sscanf(cleanHeight, "%f", &heightVal); err == nil { hasHeight = true // Convert pt to pixels (1pt = 1.33px approximately) if strings.HasSuffix(height, "pt") { heightVal *= 1.33 } } } } // If we don't have both dimensions, use fallback values if !hasWidth || !hasHeight || widthVal <= 0 || heightVal <= 0 { // Try to extract from viewBox first if strings.Contains(svgContent, `viewBox="`) { start := strings.Index(svgContent, `viewBox="`) + 9 end := strings.Index(svgContent[start:], `"`) if end > 0 { viewBox := svgContent[start : start+end] parts := strings.Fields(viewBox) if len(parts) >= 4 { if w, err := fmt.Sscanf(parts[2], "%f", &widthVal); err == nil && w == 1 && widthVal > 0 { hasWidth = true } if h, err := fmt.Sscanf(parts[3], "%f", &heightVal); err == nil && h == 1 && heightVal > 0 { hasHeight = true } } } } // Final fallback if !hasWidth || widthVal <= 0 { widthVal = 800 } if !hasHeight || heightVal <= 0 { heightVal = 600 } } // Create viewBox viewBox := fmt.Sprintf("0 0 %.0f %.0f", widthVal, heightVal) // Process the SVG content processedSVG := svgContent // Add or update viewBox if strings.Contains(processedSVG, `viewBox="`) { // Replace existing viewBox start := strings.Index(processedSVG, `viewBox="`) end := strings.Index(processedSVG[start+9:], `"`) + start + 9 processedSVG = processedSVG[:start] + fmt.Sprintf(`viewBox="%s"`, viewBox) + processedSVG[end+1:] } else { // Add viewBox to opening svg tag svgStart := strings.Index(processedSVG, "= 0 { // Find the end of the opening tag tagEnd := strings.Index(processedSVG[svgStart:], ">") + svgStart // Insert viewBox before the closing > processedSVG = processedSVG[:tagEnd] + fmt.Sprintf(` viewBox="%s"`, viewBox) + processedSVG[tagEnd:] } } // Remove or replace width and height with 100% for responsive scaling if strings.Contains(processedSVG, `width="`) { start := strings.Index(processedSVG, `width="`) end := strings.Index(processedSVG[start+7:], `"`) + start + 7 processedSVG = processedSVG[:start] + `width="100%"` + processedSVG[end+1:] } else { // Add width if it doesn't exist svgStart := strings.Index(processedSVG, "= 0 { tagEnd := strings.Index(processedSVG[svgStart:], ">") + svgStart processedSVG = processedSVG[:tagEnd] + ` width="100%"` + processedSVG[tagEnd:] } } if strings.Contains(processedSVG, `height="`) { start := strings.Index(processedSVG, `height="`) end := strings.Index(processedSVG[start+8:], `"`) + start + 8 processedSVG = processedSVG[:start] + `height="100%"` + processedSVG[end+1:] } else { // Add height if it doesn't exist svgStart := strings.Index(processedSVG, "= 0 { tagEnd := strings.Index(processedSVG[svgStart:], ">") + svgStart processedSVG = processedSVG[:tagEnd] + ` height="100%"` + processedSVG[tagEnd:] } } return processedSVG } // SVGViewerHTML creates the HTML with advanced SVG zoom and pan functionality func (tm *DAG) SVGViewerHTML(svgContent string) string { // Process SVG to ensure proper scaling processedSVG := tm.processSVGContent(svgContent) return fmt.Sprintf(` DAG Pipeline - %s

DAG Pipeline

%s - Workflow Visualization Start New Task

%s
`, tm.name, tm.name, processedSVG) } // Handlers initializes route handlers. func (tm *DAG) Handlers(app any, prefix string) { if prefix != "" && prefix != "/" { tm.httpPrefix = prefix } switch a := app.(type) { case fiber.Router: a.All("/process", tm.RenderFiber) a.Get("/request", tm.RenderFiber) a.Get("/task/status", tm.fiberTaskStatusHandler) a.Get("/dot", func(c *fiber.Ctx) error { return c.Type(fiber.MIMETextPlain).SendString(tm.ExportDOT()) }) a.Get("/", func(c *fiber.Ctx) error { image := fmt.Sprintf("%s.svg", mq.NewID()) defer os.Remove(image) if err := tm.SaveSVG(image); err != nil { return c.Status(fiber.StatusBadRequest).SendString("Failed to read request body") } svgBytes, err := os.ReadFile(image) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Could not read SVG file") } // Generate HTML with advanced SVG viewer htmlContent := tm.SVGViewerHTML(string(svgBytes)) c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.SendString(htmlContent) }) // <<< NEW FIBER API ENDPOINTS >>> a.Get("/metrics", func(c *fiber.Ctx) error { return c.JSON(tm.metrics) }) a.Get("/cancel", func(c *fiber.Ctx) error { taskID := c.Query("taskID") if taskID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": "taskID is missing"}) } err := tm.CancelTask(taskID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()}) } return c.JSON(fiber.Map{"message": "task cancelled successfully"}) }) default: http.Handle("/notify", tm.SetupWS()) http.HandleFunc("/process", tm.render) http.HandleFunc("/request", tm.render) http.HandleFunc("/task/status", tm.taskStatusHandler) http.HandleFunc("/dot", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") fmt.Fprintln(w, tm.ExportDOT()) }) // <<< NEW NET/HTTP API ENDPOINTS >>> http.HandleFunc("/metrics", tm.metricsHandler) http.HandleFunc("/cancel", tm.cancelTaskHandler) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { image := fmt.Sprintf("%s.svg", mq.NewID()) err := tm.SaveSVG(image) if err != nil { http.Error(w, "Failed to read request body", http.StatusBadRequest) return } defer os.Remove(image) svgBytes, err := os.ReadFile(image) if err != nil { http.Error(w, "Could not read SVG file", http.StatusInternalServerError) return } // Generate HTML with advanced SVG viewer htmlContent := tm.SVGViewerHTML(string(svgBytes)) w.Header().Set("Content-Type", "text/html; charset=utf-8") if _, err := w.Write([]byte(htmlContent)); err != nil { http.Error(w, "Could not write HTML response", http.StatusInternalServerError) return } }) } }