Files
Gardomatic/internal/web/web.go
T
2026-09-14 19:58:31 +02:00

133 lines
3.0 KiB
Go

package web
import (
"context"
"errors"
"fmt"
"html/template"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"
"gardomatic.kleiax.de/internal/vcs"
"gardomatic.kleiax.de/lib/client"
"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
DemoAccountEmail 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
}