phase 0 complete

This commit is contained in:
2026-08-14 11:29:11 -06:00
parent 4da16c7573
commit 85df9a208e
102 changed files with 25019 additions and 30 deletions
+108
View File
@@ -0,0 +1,108 @@
// Command solopm is the SoloPM server binary: serves the JSON API and the
// embedded Vue SPA, and hosts CLI subcommands (migrate, reindex, cull).
package main
import (
"context"
"io/fs"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/joho/godotenv"
"solopm.com/solopm-server/internal/config"
"solopm.com/solopm-server/internal/httpserver"
solopmweb "solopm.com/solopm-server/web"
)
var version = "dev"
func main() {
// Best-effort: .env is a local-dev convenience, absent in Docker/prod
// where real env vars are set directly.
_ = godotenv.Load()
if len(os.Args) > 1 {
switch os.Args[1] {
case "migrate":
runMigrateCmd(os.Args[2:])
return
case "serve":
// falls through to default serve below
default:
slog.Error("unknown subcommand", "subcommand", os.Args[1])
os.Exit(1)
}
}
if err := serve(); err != nil {
slog.Error("server exited with error", "error", err)
os.Exit(1)
}
}
func serve() error {
cfg, err := config.Load()
if err != nil {
return err
}
logLevel := slog.LevelInfo
if cfg.LogLevel == "debug" {
logLevel = slog.LevelDebug
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel}))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
if err != nil {
return err
}
defer pool.Close()
if err := runMigrations(cfg.DatabaseURL); err != nil {
return err
}
spaFS, err := fs.Sub(solopmweb.DistFS, "dist")
if err != nil {
return err
}
router := httpserver.New(httpserver.Options{
Config: cfg,
Logger: logger,
DB: pool,
Version: version,
Dev: cfg.LogLevel == "debug",
SPA: spaFS,
})
srv := &http.Server{
Addr: ":" + cfg.HTTPPort,
Handler: router,
}
go func() {
logger.Info("listening", "port", cfg.HTTPPort, "version", version)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("listen error", "error", err)
}
}()
<-ctx.Done()
logger.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx)
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"fmt"
"os"
"solopm.com/solopm-server/internal/config"
"solopm.com/solopm-server/internal/db"
)
// runMigrations applies pending migrations at startup. Called by serve()
// unless a future --no-migrate flag disables it.
func runMigrations(databaseURL string) error {
return db.MigrateUp(databaseURL)
}
// runMigrateCmd implements `solopm migrate up|down`.
func runMigrateCmd(args []string) {
if len(args) != 1 || (args[0] != "up" && args[0] != "down") {
fmt.Fprintln(os.Stderr, "usage: solopm migrate up|down")
os.Exit(1)
}
cfg, err := config.Load()
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
switch args[0] {
case "up":
err = db.MigrateUp(cfg.DatabaseURL)
case "down":
err = db.MigrateDown(cfg.DatabaseURL)
}
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Println("migrate " + args[0] + ": ok")
}