// Package main is the CarrotAssistant backend entrypoint. // // It loads configuration, opens the database, bootstraps an initial admin // account if none exists, and serves HTTP until interrupted. package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "code.littlelan.cn/CarrotAssistant/backend/internal/config" "code.littlelan.cn/CarrotAssistant/backend/internal/security" "code.littlelan.cn/CarrotAssistant/backend/internal/server" "code.littlelan.cn/CarrotAssistant/backend/internal/store" ) func main() { cfg, err := config.Load() if err != nil { log.Fatalf("load config: %v", err) } log.Printf("data dir: %s", cfg.DataDir) db, err := store.Open(cfg.DBPath) if err != nil { log.Fatalf("open db: %v", err) } crypto, err := security.NewCrypto(cfg.MasterKey) if err != nil { log.Fatalf("init crypto: %v", err) } // Bootstrap an admin account on first run so the console is reachable. // Production should set ADMIN_BOOTSTRAP_USER / ADMIN_BOOTSTRAP_PASSWORD. if cfg.AdminBootstrapUser != "" { if err := server.EnsureAdmin(db, cfg.AdminBootstrapUser, cfg.AdminBootstrapPassword); err != nil { log.Printf("ensure admin: %v", err) } } deps := server.Deps{ DB: db, Config: cfg, Crypto: crypto, SkillsDir: cfg.SkillsDir, } handler := server.New(deps) srv := &http.Server{ Addr: cfg.HTTPAddr, Handler: handler, ReadHeaderTimeout: 10 * time.Second, } go func() { log.Printf("listening on %s", cfg.HTTPAddr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server: %v", err) } }() // Graceful shutdown on SIGINT / SIGTERM. stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop log.Println("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { fmt.Fprintf(os.Stderr, "shutdown: %v\n", err) } }