package api import ( "net/http" "github.com/google/uuid" "markdownhub/internal/crypto" "markdownhub/internal/files" ) // ─── Build Jobs ────────────────────────────────────────────────────────────── func (s *Server) handleBuildSubmit(w http.ResponseWriter, r *http.Request) { var req struct { FileID string `json:"file_id"` Path string `json:"path"` GiteaURL string `json:"gitea_url"` GiteaToken string `json:"gitea_token"` GiteaOrg string `json:"gitea_org"` RepoName string `json:"repo_name"` Model string `json:"model"` } if err := decodeBody(r, &req); err != nil || req.RepoName == "" { writeJSON(w, 400, map[string]string{"error": "repo_name required"}) return } userID := getUserID(r) // Read spec content from file var specContent string if req.Path != "" { content, err := files.ReadFile(s.dataDir, userID, req.Path) if err != nil { writeJSON(w, 404, map[string]string{"error": "spec file not found"}) return } specContent = content } jobID := uuid.New().String() // Encrypt Gitea token at rest encToken := req.GiteaToken if encToken != "" { key := crypto.KeyFromSecret(s.secret) if enc, err := crypto.EncryptString(encToken, key); err == nil { encToken = enc } } _, err := s.db.Exec( `INSERT INTO build_jobs (id, user_id, status, spec_content, gitea_url, gitea_token, gitea_org, repo_name, model) VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?)`, jobID, userID, specContent, req.GiteaURL, encToken, req.GiteaOrg, req.RepoName, req.Model, ) if err != nil { writeJSON(w, 500, map[string]string{"error": "failed to create job"}) return } writeJSON(w, 201, map[string]string{"job_id": jobID, "status": "pending"}) } func (s *Server) handleBuildJobs(w http.ResponseWriter, r *http.Request) { userID := getUserID(r) rows, err := s.db.Query( "SELECT id, status, repo_name, model, created_at, updated_at FROM build_jobs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50", userID, ) if err != nil { writeJSON(w, 500, map[string]string{"error": "query failed"}) return } defer rows.Close() var jobs []map[string]interface{} for rows.Next() { var id, status, repoName, model, createdAt, updatedAt string rows.Scan(&id, &status, &repoName, &model, &createdAt, &updatedAt) jobs = append(jobs, map[string]interface{}{ "job_id": id, "status": status, "repo_name": repoName, "model": model, "created_at": createdAt, "updated_at": updatedAt, }) } if jobs == nil { jobs = []map[string]interface{}{} } writeJSON(w, 200, jobs) } func (s *Server) handleBuildStatus(w http.ResponseWriter, r *http.Request) { var req struct { JobID string `json:"job_id"` } if err := decodeBody(r, &req); err != nil || req.JobID == "" { writeJSON(w, 400, map[string]string{"error": "job_id required"}) return } var status, repoName, log, updatedAt string err := s.db.QueryRow( "SELECT status, repo_name, log, updated_at FROM build_jobs WHERE id = ?", req.JobID, ).Scan(&status, &repoName, &log, &updatedAt) if err != nil { writeJSON(w, 404, map[string]string{"error": "job not found"}) return } writeJSON(w, 200, map[string]interface{}{ "job_id": req.JobID, "status": status, "repo_name": repoName, "log": log, "updated_at": updatedAt, }) } func (s *Server) handleBuildCancel(w http.ResponseWriter, r *http.Request) { var req struct { JobID string `json:"job_id"` } if err := decodeBody(r, &req); err != nil || req.JobID == "" { writeJSON(w, 400, map[string]string{"error": "job_id required"}) return } s.db.Exec("UPDATE build_jobs SET status = 'cancelled', updated_at = datetime('now') WHERE id = ? AND status = 'pending'", req.JobID) writeJSON(w, 200, map[string]string{"status": "cancelled"}) } // ─── Daemon Endpoints ──────────────────────────────────────────────────────── func (s *Server) handleDaemonPoll(w http.ResponseWriter, r *http.Request) { var id, specContent, giteaURL, giteaToken, giteaOrg, repoName, model string err := s.db.QueryRow( `SELECT id, spec_content, gitea_url, gitea_token, gitea_org, repo_name, model FROM build_jobs WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1`, ).Scan(&id, &specContent, &giteaURL, &giteaToken, &giteaOrg, &repoName, &model) if err != nil { // No pending jobs writeJSON(w, 200, map[string]interface{}{}) return } // Mark as picked up s.db.Exec("UPDATE build_jobs SET status = 'running', updated_at = datetime('now') WHERE id = ?", id) // Decrypt token if giteaToken != "" { key := crypto.KeyFromSecret(s.secret) if dec, err := crypto.DecryptString(giteaToken, key); err == nil { giteaToken = dec } } writeJSON(w, 200, map[string]interface{}{ "job_id": id, "spec_content": specContent, "gitea_url": giteaURL, "gitea_token": giteaToken, "gitea_org": giteaOrg, "repo_name": repoName, "model": model, }) } func (s *Server) handleDaemonHeartbeat(w http.ResponseWriter, r *http.Request) { s.db.Exec( `INSERT INTO daemon_state (id, last_heartbeat, status) VALUES ('singleton', datetime('now'), 'online') ON CONFLICT(id) DO UPDATE SET last_heartbeat = datetime('now'), status = 'online'`, ) writeJSON(w, 200, map[string]string{"status": "ok"}) } func (s *Server) handleDaemonReport(w http.ResponseWriter, r *http.Request) { var req struct { JobID string `json:"job_id"` Status string `json:"status"` Log string `json:"log"` } if err := decodeBody(r, &req); err != nil || req.JobID == "" { writeJSON(w, 400, map[string]string{"error": "job_id required"}) return } s.db.Exec( "UPDATE build_jobs SET status = ?, log = ?, updated_at = datetime('now') WHERE id = ?", req.Status, req.Log, req.JobID, ) writeJSON(w, 200, map[string]string{"status": "ok"}) } // ─── File Search ───────────────────────────────────────────────────────────── func (s *Server) handleSearchFiles(w http.ResponseWriter, r *http.Request) { var req struct { Query string `json:"query"` } if err := decodeBody(r, &req); err != nil || req.Query == "" { writeJSON(w, 400, map[string]string{"error": "query required"}) return } userID := getUserID(r) results, err := files.Search(s.dataDir, userID, req.Query) if err != nil { writeJSON(w, 500, map[string]string{"error": "search failed"}) return } writeJSON(w, 200, results) }