Server picks variant from Lisbon-local date (rain/petals/jacaranda/motes/leaves) and renders behind content via a canvas particle layer. Each variant has a hand-drawn silhouette so shapes are recognizable. /weather demo route exposes variant + intensity + phase pickers, locking the time-of-day phase override.
108 lines
3.1 KiB
Go
108 lines
3.1 KiB
Go
// Package web serves Pete's read-only news interface.
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"errors"
|
|
"html/template"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"pete/internal/config"
|
|
)
|
|
|
|
//go:embed templates/*.html
|
|
var templateFS embed.FS
|
|
|
|
//go:embed static
|
|
var staticFS embed.FS
|
|
|
|
// Channel describes one section of the site.
|
|
type Channel struct {
|
|
Slug string // "gaming"
|
|
Title string // "Gaming"
|
|
Theme string // tailwind theme key: "gaming" | "tech" | "politics"
|
|
Emoji string
|
|
Blurb string
|
|
}
|
|
|
|
// channels in display order. Add a new entry here to add a section.
|
|
var channels = []Channel{
|
|
{Slug: "gaming", Title: "Gaming", Theme: "gaming", Emoji: "🎮", Blurb: "Releases, platforms, and the people making the games."},
|
|
{Slug: "tech", Title: "Tech", Theme: "tech", Emoji: "💻", Blurb: "Industry, products, and the wires that hold it all together."},
|
|
{Slug: "politics", Title: "Politics", Theme: "politics", Emoji: "🏛️", Blurb: "Policy, power, and the news of the day."},
|
|
{Slug: "music", Title: "Music", Theme: "music", Emoji: "🎵", Blurb: "Records, scenes, and the artists shaping the sound."},
|
|
}
|
|
|
|
// Server is the HTTP server for Pete's web UI.
|
|
type Server struct {
|
|
cfg config.WebConfig
|
|
srv *http.Server
|
|
tpls map[string]*template.Template // keyed by page name (e.g. "index", "channel")
|
|
}
|
|
|
|
// New builds the server. Templates are parsed once at startup. Each page
|
|
// gets its own template set sharing layout.html + _card.html, which avoids
|
|
// `main` define collisions between pages.
|
|
func New(cfg config.WebConfig) (*Server, error) {
|
|
pages := []string{"index", "channel", "weather"}
|
|
tpls := make(map[string]*template.Template, len(pages))
|
|
for _, p := range pages {
|
|
t, err := template.New("").Funcs(funcs).ParseFS(templateFS,
|
|
"templates/layout.html",
|
|
"templates/_card.html",
|
|
"templates/"+p+".html",
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tpls[p] = t
|
|
}
|
|
s := &Server{cfg: cfg, tpls: tpls}
|
|
mux := http.NewServeMux()
|
|
|
|
staticSub, err := fs.Sub(staticFS, "static")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
|
|
mux.HandleFunc("GET /img/{name}", s.handleImg)
|
|
|
|
mux.HandleFunc("GET /{$}", s.handleIndex)
|
|
mux.HandleFunc("GET /weather", s.handleWeatherDemo)
|
|
for _, ch := range channels {
|
|
ch := ch
|
|
mux.HandleFunc("GET /"+ch.Slug, func(w http.ResponseWriter, r *http.Request) {
|
|
s.handleChannel(w, r, ch)
|
|
})
|
|
}
|
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("ok"))
|
|
})
|
|
|
|
s.srv = &http.Server{
|
|
Addr: cfg.ListenAddr,
|
|
Handler: mux,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// Start runs the HTTP server and blocks until ctx is canceled.
|
|
func (s *Server) Start(ctx context.Context) {
|
|
go func() {
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = s.srv.Shutdown(shutdownCtx)
|
|
}()
|
|
slog.Info("web server listening", "addr", s.cfg.ListenAddr)
|
|
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
slog.Error("web server crashed", "err", err)
|
|
}
|
|
}
|