package ui import ( "context" "log" "net/http" "strconv" "strings" "github.com/hireme/hireme/internal/config" "github.com/hireme/hireme/internal/db" "github.com/hireme/hireme/internal/service" ) type Server struct { cfg *config.Config db *db.DB pipeline *service.PipelineService } func NewServer(cfg *config.Config, database *db.DB, pipeline *service.PipelineService) *Server { return &Server{ cfg: cfg, db: database, pipeline: pipeline, } } func (s *Server) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /", s.handleIndex) mux.HandleFunc("GET /targets", s.handleGetTargets) mux.HandleFunc("POST /targets", s.handleCreateTarget) mux.HandleFunc("POST /targets/{id}/toggle", s.handleToggleTarget) mux.HandleFunc("GET /jobs", s.handleGetJobs) mux.HandleFunc("GET /jobs/{id}", s.handleGetJobDetail) mux.HandleFunc("POST /jobs/{id}/analyze", s.handleAnalyzeJob) mux.HandleFunc("POST /applications", s.handleCreateApplication) mux.HandleFunc("PATCH /applications/{id}/status", s.handleUpdateAppStatus) mux.HandleFunc("POST /pipeline/run", s.handleRunPipeline) } func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } targets, err := s.db.GetAllTargets() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } jobs, err := s.db.GetJobsWithAnalysis() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } apps, err := s.db.GetApplications() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } qualifiedCount := 0 for _, j := range jobs { if j.Analysis != nil && j.Analysis.IsQualified { qualifiedCount++ } } data := PageData{ Targets: targets, Jobs: jobs, Applications: apps, ActiveTab: "triage", LLMProvider: s.cfg.LLMProvider, QualifiedJobsCount: qualifiedCount, TotalIngestedCount: len(jobs), ActiveApplications: len(apps), } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := RenderPage(w, data); err != nil { log.Printf("[UI] RenderPage error: %v", err) } } func (s *Server) handleGetTargets(w http.ResponseWriter, r *http.Request) { targets, err := s.db.GetAllTargets() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = RenderTargetList(w, targets) } func (s *Server) handleCreateTarget(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } name := r.FormValue("name") targetURL := r.FormValue("target_url") platform := r.FormValue("platform") if name == "" || targetURL == "" { http.Error(w, "name and target_url are required", http.StatusBadRequest) return } _, err := s.db.CreateTarget(name, targetURL, platform) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } s.handleGetTargets(w, r) } func (s *Server) handleToggleTarget(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "invalid target id", http.StatusBadRequest) return } if err := s.db.ToggleTargetStatus(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } s.handleGetTargets(w, r) } func (s *Server) handleGetJobs(w http.ResponseWriter, r *http.Request) { jobs, err := s.db.GetJobsWithAnalysis() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = RenderJobList(w, jobs) } func (s *Server) handleGetJobDetail(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "invalid job id", http.StatusBadRequest) return } job, err := s.db.GetJobByID(id) if err != nil { http.Error(w, "job not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = RenderJobDetail(w, job) } func (s *Server) handleAnalyzeJob(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "invalid job id", http.StatusBadRequest) return } job, err := s.pipeline.AnalyzeJobByID(r.Context(), id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = RenderJobDetail(w, job) } func (s *Server) handleCreateApplication(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } rawJobIDStr := r.FormValue("raw_job_id") status := r.FormValue("status") if status == "" { status = "SAVED" } rawJobID, err := strconv.ParseInt(rawJobIDStr, 10, 64) if err != nil { http.Error(w, "invalid raw_job_id", http.StatusBadRequest) return } _, err = s.db.CreateOrUpdateApplication(rawJobID, status, "") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if strings.Contains(r.Header.Get("HX-Target"), "kanban") { apps, _ := s.db.GetApplications() w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = RenderKanbanBoard(w, apps) return } s.handleGetJobs(w, r) } func (s *Server) handleUpdateAppStatus(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") appID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "invalid app id", http.StatusBadRequest) return } if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } newStatus := r.FormValue("status") if newStatus == "" { http.Error(w, "status required", http.StatusBadRequest) return } // Fetch rawJobID for appID apps, err := s.db.GetApplications() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var targetRawJobID int64 = -1 for _, a := range apps { if a.App != nil && a.App.ID == appID { targetRawJobID = a.ID break } } if targetRawJobID == -1 { http.Error(w, "application not found", http.StatusNotFound) return } _, err = s.db.CreateOrUpdateApplication(targetRawJobID, newStatus, "") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } updatedApps, _ := s.db.GetApplications() w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = RenderKanbanBoard(w, updatedApps) } func (s *Server) handleRunPipeline(w http.ResponseWriter, r *http.Request) { ctx := context.Background() _, _, err := s.pipeline.RunPipeline(ctx) if err != nil { log.Printf("[Server] Pipeline run error: %v", err) } s.handleGetJobs(w, r) }