felt/cmd/leaf/main.go
Mikkel Georgsen ae90d9bfae feat(01-04): add clock warnings, API routes, tests, and server wiring
- Clock API routes: start, pause, resume, advance, rewind, jump, get, warnings
- Role-based access control (floor+ for mutations, any auth for reads)
- Clock state persistence callback to DB on meaningful changes
- Blind structure levels loaded from DB on clock start
- Clock registry wired into HTTP server and cmd/leaf main
- 25 tests covering: state machine, countdown, pause/resume, auto-advance,
  jump, rewind, hand-for-hand, warnings, overtime, crash recovery, snapshot
- Fix missing crypto/rand import in auth/pin.go (Rule 3 auto-fix)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 03:56:23 +01:00

137 lines
3.9 KiB
Go

// Command leaf is the Felt tournament engine binary. It starts all embedded
// infrastructure (LibSQL, NATS JetStream, WebSocket hub) and serves the
// SvelteKit SPA over HTTP.
package main
import (
"context"
"flag"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
feltauth "github.com/felt-app/felt/internal/auth"
"github.com/felt-app/felt/internal/clock"
feltnats "github.com/felt-app/felt/internal/nats"
"github.com/felt-app/felt/internal/server"
"github.com/felt-app/felt/internal/server/middleware"
"github.com/felt-app/felt/internal/server/ws"
"github.com/felt-app/felt/internal/store"
)
func main() {
dataDir := flag.String("data-dir", "./data", "Data directory for database and files")
addr := flag.String("addr", ":8080", "HTTP listen address")
devMode := flag.Bool("dev", false, "Enable development mode (permissive CORS, dev seed data)")
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
log.SetPrefix("felt: ")
log.Printf("starting (data-dir=%s, addr=%s, dev=%v)", *dataDir, *addr, *devMode)
// Create root context with cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// ---- 1. LibSQL Database ----
db, err := store.Open(*dataDir, *devMode)
if err != nil {
log.Fatalf("failed to open database: %v", err)
}
defer db.Close()
// Verify database is working
var one int
if err := db.QueryRow("SELECT 1").Scan(&one); err != nil {
log.Fatalf("database health check failed: %v", err)
}
log.Printf("database ready")
// ---- 2. Embedded NATS Server ----
natsServer, err := feltnats.Start(ctx, *dataDir)
if err != nil {
log.Fatalf("failed to start NATS: %v", err)
}
defer natsServer.Shutdown()
// ---- 3. JWT Signing Key (persisted in LibSQL) ----
signingKey, err := feltauth.LoadOrCreateSigningKey(db.DB)
if err != nil {
log.Fatalf("failed to load/create signing key: %v", err)
}
// ---- 4. Auth Service ----
jwtService := feltauth.NewJWTService(signingKey, 7*24*time.Hour) // 7-day expiry
authService := feltauth.NewAuthService(db.DB, jwtService)
// ---- 5. WebSocket Hub ----
tokenValidator := func(tokenStr string) (string, string, error) {
return middleware.ValidateJWT(tokenStr, signingKey)
}
// Tournament validator stub -- allows all for now
// TODO: Implement tournament existence + access check against DB
tournamentValidator := func(tournamentID string, operatorID string) error {
return nil // Accept all tournaments for now
}
var allowedOrigins []string
if *devMode {
allowedOrigins = nil // InsecureSkipVerify will be used
} else {
allowedOrigins = []string{"*"} // Same-origin enforced by browser
}
hub := ws.NewHub(tokenValidator, tournamentValidator, allowedOrigins)
defer hub.Shutdown()
// ---- 6. Clock Registry ----
clockRegistry := clock.NewRegistry(hub)
defer clockRegistry.Shutdown()
log.Printf("clock registry ready")
// ---- 7. HTTP Server ----
srv := server.New(server.Config{
Addr: *addr,
SigningKey: signingKey,
DevMode: *devMode,
}, db.DB, natsServer.Server(), hub, authService, clockRegistry)
// Start HTTP server in goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
log.Printf("ready (addr=%s, dev=%v)", *addr, *devMode)
// ---- Signal Handling ----
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh
log.Printf("received signal: %s, shutting down...", sig)
// Graceful shutdown in reverse startup order
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
// 7. HTTP Server
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
}
// 6. Clock Registry (closed by defer)
// 5. WebSocket Hub (closed by defer)
// 4. NATS Server (closed by defer)
// 3. Database (closed by defer)
cancel() // Cancel root context
log.Printf("shutdown complete")
}