package web import ( "context" "errors" "fmt" "html/template" "log/slog" "net" "net/http" "os" "os/signal" "strconv" "sync" "syscall" "time" "gardomatic.kleiax.de/lib/client" "gardomatic.kleiax.de/internal/vcs" "github.com/go-playground/form/v4" ) var ( version = vcs.Version() ) // Config contains web listener, API endpoint, and browser cookie settings. type Config struct { Host string Port int Env string APIBaseURL string SessionCookieName string CookieSecure bool } type application struct { config Config logger *slog.Logger templateCache map[string]*template.Template formDecoder *form.Decoder apiClient *client.Client wg sync.WaitGroup } // New creates a web application configured to communicate with the Gardomatic API. func New(cfg Config) (*application, error) { var handler slog.Handler = slog.NewTextHandler(os.Stdout, nil) if cfg.Env == "production" { handler = slog.NewJSONHandler(os.Stdout, nil) } logger := slog.New(handler) templateCache, err := newTemplateCache() if err != nil { return nil, fmt.Errorf("create template cache: %w", err) } formDecoder := form.NewDecoder() options := []client.Option{} if cfg.SessionCookieName != "" { options = append(options, client.WithSessionCookieName(cfg.SessionCookieName)) } apiClient, err := client.New(cfg.APIBaseURL, options...) if err != nil { return nil, fmt.Errorf("create API client: %w", err) } app := &application{ templateCache: templateCache, formDecoder: formDecoder, config: cfg, logger: logger, apiClient: apiClient, } return app, nil } // Run starts the web server and blocks until it shuts down or encounters an error. func (app *application) Run() { err := app.serve() if err != nil { app.logger.Error(err.Error()) os.Exit(1) } } // Version returns the build version used by the web binary. func (app *application) Version() string { return fmt.Sprintf("Version:\t%s\n", version) } func (app *application) serve() error { srv := &http.Server{ Addr: net.JoinHostPort(app.config.Host, strconv.Itoa(app.config.Port)), Handler: app.routes(), IdleTimeout: time.Minute, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 2 * time.Minute, WriteTimeout: 2 * time.Minute, } shutdownError := make(chan error) go func() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) s := <-quit app.logger.Info("stopping server", "addr", srv.Addr, "signal", s.String()) shutdownError <- srv.Shutdown(context.Background()) }() app.logger.Info("starting server", "addr", srv.Addr, "env", app.config.Env) err := srv.ListenAndServe() if !errors.Is(err, http.ErrServerClosed) { return err } err = <-shutdownError if err != nil { return err } app.logger.Info("waiting for background tasks") app.wg.Wait() app.logger.Info("shutdown complete") return nil }