package main import ( "net/http" "strings" "fn-registry/functions/infra" ) const maxBodyBytes = 1 << 20 // 1 MiB func badRequest(w http.ResponseWriter, msg string) { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusBadRequest, Code: "bad_request", Message: msg}) } func notFound(w http.ResponseWriter, msg string) { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusNotFound, Code: "not_found", Message: msg}) } func serverError(w http.ResponseWriter, err error) { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusInternalServerError, Code: "internal", Message: err.Error()}) } // GET /api/board → { columns: [...], cards: [...] } func handleGetBoard(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cols, err := db.ListColumns() if err != nil { serverError(w, err) return } cards, err := db.ListCardsWithTime() if err != nil { serverError(w, err) return } infra.HTTPJSONResponse(w, http.StatusOK, map[string]any{ "columns": cols, "cards": cards, }) } } // POST /api/columns { name } func handleCreateColumn(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct{ Name string `json:"name"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } if strings.TrimSpace(body.Name) == "" { badRequest(w, "name required") return } c, err := db.CreateColumn(body.Name) if err != nil { serverError(w, err) return } infra.HTTPJSONResponse(w, http.StatusCreated, c) } } // PATCH /api/columns/{id} { name?, position?, location?, width? } func handleUpdateColumn(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var body struct { Name *string `json:"name"` Position *int `json:"position"` Location *string `json:"location"` Width *int `json:"width"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } if err := db.UpdateColumn(id, ColumnPatch{Name: body.Name, Position: body.Position, Location: body.Location, Width: body.Width}); err != nil { serverError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // DELETE /api/columns/{id} func handleDeleteColumn(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := db.DeleteColumn(id); err != nil { serverError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // POST /api/columns/reorder { ids: [...] } func handleReorderColumns(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct{ IDs []string `json:"ids"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } if err := db.ReorderColumns(body.IDs); err != nil { serverError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // POST /api/cards { column_id, requester?, title, description? } func handleCreateCard(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct { ColumnID string `json:"column_id"` Requester string `json:"requester"` Title string `json:"title"` Description string `json:"description"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } if body.ColumnID == "" || strings.TrimSpace(body.Title) == "" { badRequest(w, "column_id and title required") return } c, err := db.CreateCard(body.ColumnID, body.Requester, body.Title, body.Description) if err != nil { serverError(w, err) return } infra.HTTPJSONResponse(w, http.StatusCreated, c) } } // PATCH /api/cards/{id} { requester?, title?, description?, color? } func handleUpdateCard(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var body struct { Requester *string `json:"requester"` Title *string `json:"title"` Description *string `json:"description"` Color *string `json:"color"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } if err := db.UpdateCard(id, CardPatch{Requester: body.Requester, Title: body.Title, Description: body.Description, Color: body.Color}); err != nil { serverError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // DELETE /api/cards/{id} func handleDeleteCard(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := db.DeleteCard(id); err != nil { serverError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // POST /api/cards/{id}/move { column_id, ordered_ids } func handleMoveCard(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var body struct { ColumnID string `json:"column_id"` OrderedIDs []string `json:"ordered_ids"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } if body.ColumnID == "" { badRequest(w, "column_id required") return } if err := db.MoveCard(id, body.ColumnID, body.OrderedIDs); err != nil { if strings.Contains(err.Error(), "not found") { notFound(w, "card not found") return } serverError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // GET /api/cards/{id}/history → [HistoryEntry, ...] func handleCardHistory(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") entries, err := db.CardHistory(id) if err != nil { serverError(w, err) return } infra.HTTPJSONResponse(w, http.StatusOK, entries) } } func apiRoutes(db *DB, chatWorkdir string, logger *ChatLogger) []infra.Route { return []infra.Route{ {Method: "GET", Path: "/api/board", Handler: handleGetBoard(db)}, {Method: "POST", Path: "/api/columns", Handler: handleCreateColumn(db)}, {Method: "POST", Path: "/api/columns/reorder", Handler: handleReorderColumns(db)}, {Method: "PATCH", Path: "/api/columns/{id}", Handler: handleUpdateColumn(db)}, {Method: "DELETE", Path: "/api/columns/{id}", Handler: handleDeleteColumn(db)}, {Method: "POST", Path: "/api/cards", Handler: handleCreateCard(db)}, {Method: "PATCH", Path: "/api/cards/{id}", Handler: handleUpdateCard(db)}, {Method: "DELETE", Path: "/api/cards/{id}", Handler: handleDeleteCard(db)}, {Method: "POST", Path: "/api/cards/{id}/move", Handler: handleMoveCard(db)}, {Method: "GET", Path: "/api/cards/{id}/history", Handler: handleCardHistory(db)}, {Method: "POST", Path: "/api/chat", Handler: handleChat(db, chatWorkdir, logger)}, } }