package main import ( "crypto/rand" "encoding/hex" "log" "net/http" "os" "strings" "gpt-plus/internal/db" "gpt-plus/internal/handler" "gpt-plus/internal/task" "gpt-plus/web" "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) func main() { _ = godotenv.Load() port := os.Getenv("PORT") if port == "" { port = "8080" } dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "./gptplus.db" } jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" { b := make([]byte, 32) rand.Read(b) jwtSecret = hex.EncodeToString(b) log.Println("[init] JWT_SECRET not set, using random secret (sessions won't survive restart)") } encKey := os.Getenv("ENCRYPTION_KEY") if encKey != "" { if err := db.SetEncryptionKey(encKey); err != nil { log.Fatalf("[init] invalid ENCRYPTION_KEY: %v", err) } } devMode := os.Getenv("DEV_MODE") == "true" if devMode { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } // Init database if err := db.Init(dbPath); err != nil { log.Fatalf("[init] database init failed: %v", err) } log.Printf("[init] database ready: %s", dbPath) // Init auth handler.SetJWTSecret(jwtSecret) if err := handler.InitAdminPassword(); err != nil { log.Fatalf("[init] admin password init failed: %v", err) } // Seed default config handler.SeedDefaultConfigs() // Init task manager tm := task.NewTaskManager(db.GetDB()) tm.Init() handler.SetTaskManager(tm) // Setup router r := handler.SetupRouter(devMode) // Serve frontend in production mode if !devMode { serveFrontend(r) } log.Printf("[init] server starting on :%s (dev=%v)", port, devMode) if err := r.Run(":" + port); err != nil { log.Fatalf("server error: %v", err) } } func serveFrontend(r *gin.Engine) { frontendFS, err := web.FrontendFS() if err != nil { log.Printf("[init] frontend not embedded, skipping static file serving") return } // Serve static assets httpFS := http.FS(frontendFS) r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path // API routes that don't match return 404 JSON if strings.HasPrefix(path, "/api/") { c.JSON(http.StatusNotFound, gin.H{"error": "API route not found"}) return } // Try to serve the file directly trimmed := strings.TrimPrefix(path, "/") if trimmed == "" { trimmed = "index.html" } if f, err := frontendFS.Open(trimmed); err == nil { f.Close() c.FileFromFS(path, httpFS) return } // SPA fallback: serve index.html for all non-file routes c.FileFromFS("/", httpFS) }) }