Phase 0: project scaffold
- Go module + chi server with embedded SPA serving and /api/health - internal/config env loader (local-dev defaults; auth/copyleaks deferred) - React 19 + Vite 6 + Tailwind v4 frontend with full Petal design tokens - Frontend embedded into the binary via web/embed.go (go:embed all:dist) - README dev workflow, .env.example, BUILD_PLAN progress tracker - Verified end-to-end: binary serves health, embedded SPA, and SPA fallback
This commit is contained in:
77
cmd/server/main.go
Normal file
77
cmd/server/main.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"gitea.parodia.dev/drwily/petal/internal/config"
|
||||
"gitea.parodia.dev/drwily/petal/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Route("/api", func(api chi.Router) {
|
||||
api.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
})
|
||||
|
||||
// Everything else: serve the embedded SPA (with index.html fallback for client routing).
|
||||
r.NotFound(spaHandler())
|
||||
|
||||
addr := ":" + cfg.Port
|
||||
log.Printf("petal listening on %s (LLM backend=%s)", addr, cfg.LLMBackend)
|
||||
if err := http.ListenAndServe(addr, r); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// spaHandler serves the embedded web/dist as a single-page app: static files
|
||||
// when they exist, falling back to index.html for unknown paths. If the
|
||||
// frontend hasn't been built yet, it returns a friendly dev hint instead.
|
||||
func spaHandler() http.HandlerFunc {
|
||||
sub, err := fs.Sub(web.DistFS, "dist")
|
||||
if err != nil {
|
||||
log.Fatalf("embed sub: %v", err)
|
||||
}
|
||||
|
||||
if _, err := fs.Stat(sub, "index.html"); errors.Is(err, fs.ErrNotExist) {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("Petal backend is running, but the frontend isn't built yet.\n" +
|
||||
"Run `npm run build` in web/, or use `npm run dev` for the dev server on :5173.\n"))
|
||||
}
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(http.FS(sub))
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
p := strings.TrimPrefix(req.URL.Path, "/")
|
||||
if p == "" {
|
||||
p = "index.html"
|
||||
}
|
||||
if _, err := fs.Stat(sub, p); errors.Is(err, fs.ErrNotExist) {
|
||||
// Unknown path → let the SPA router handle it.
|
||||
req2 := new(http.Request)
|
||||
*req2 = *req
|
||||
req2.URL.Path = "/"
|
||||
fileServer.ServeHTTP(w, req2)
|
||||
return
|
||||
}
|
||||
fileServer.ServeHTTP(w, req)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user