// Package server sets up and runs the HTTP server. package server import ( "context" "embed" "fmt" "html/template" "io/fs" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/luxick/chronological/internal/config" "github.com/luxick/chronological/internal/handlers" "github.com/luxick/chronological/internal/storage" ) // TemplatesFS and StaticFS are set from main package where embed works var TemplatesFS embed.FS var StaticFS embed.FS // Server represents the HTTP server and its dependencies. type Server struct { config *config.Config httpServer *http.Server templates *template.Template icsStore *storage.ICSStore photoStore *storage.PhotoStore caldavStore *storage.CalDAVStore } // New creates a new server with the given configuration. func New(cfg *config.Config) (*Server, error) { s := &Server{ config: cfg, icsStore: storage.NewICSStore(cfg.Calendar.LocalICS, cfg.Diary.ICSFile), photoStore: storage.NewPhotoStore(cfg.Photos.RootFolder), } // Initialize CalDAV store if enabled if cfg.CalDAV.Enabled { s.caldavStore = storage.NewCalDAVStore(cfg.CalDAV.URL, cfg.CalDAV.Username, cfg.CalDAV.Password) } // Parse templates if err := s.parseTemplates(); err != nil { return nil, fmt.Errorf("failed to parse templates: %w", err) } return s, nil } func (s *Server) parseTemplates() error { funcMap := template.FuncMap{ "formatDate": formatDate, "formatTime": formatTime, "formatDateTime": formatDateTime, "formatMonth": formatMonth, "daysInMonth": daysInMonth, "weekday": weekday, "add": add, "sub": sub, "seq": seq, "isToday": isToday, "isSameMonth": isSameMonth, } // Get the templates subdirectory templatesDir, err := fs.Sub(TemplatesFS, "templates") if err != nil { return fmt.Errorf("failed to get templates subdirectory: %w", err) } tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesDir, "layouts/*.html", "partials/*.html", "pages/*.html", ) if err != nil { return fmt.Errorf("failed to parse templates: %w", err) } s.templates = tmpl return nil } // Run starts the HTTP server and blocks until shutdown. func (s *Server) Run() error { // Connect to CalDAV if enabled if s.caldavStore != nil { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := s.caldavStore.Connect(ctx); err != nil { log.Printf("Warning: failed to connect to CalDAV server: %v", err) } } // Create handlers h := handlers.New(s.templates, s.icsStore, s.photoStore, s.caldavStore) // Set up routes mux := http.NewServeMux() // Static files staticDir, err := fs.Sub(StaticFS, "static") if err != nil { return fmt.Errorf("failed to get static subdirectory: %w", err) } mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticDir)))) // Photo files (served from configured directory) if s.config.Photos.RootFolder != "" { mux.Handle("GET /photos/", http.StripPrefix("/photos/", http.FileServer(http.Dir(s.config.Photos.RootFolder)))) } // Page routes mux.HandleFunc("GET /", h.HandleIndex) mux.HandleFunc("GET /calendar/{year}/{month}", h.HandleCalendar) mux.HandleFunc("GET /timeline/{year}/{month}", h.HandleTimeline) mux.HandleFunc("GET /day/{year}/{month}/{day}", h.HandleDay) // Diary routes mux.HandleFunc("GET /diary/{date}", h.HandleGetDiary) mux.HandleFunc("POST /diary/{date}", h.HandleSaveDiary) mux.HandleFunc("DELETE /diary/{date}", h.HandleDeleteDiary) // Create HTTP server s.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", s.config.Server.Port), Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } // Start server in goroutine go func() { log.Printf("Starting server on http://localhost:%d", s.config.Server.Port) if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } }() // Wait for shutdown signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := s.httpServer.Shutdown(ctx); err != nil { return fmt.Errorf("server shutdown error: %w", err) } log.Println("Server stopped") return nil } // Template helper functions func formatDate(t time.Time) string { return t.Format("2006-01-02") } func formatTime(t time.Time) string { return t.Format("15:04") } func formatDateTime(t time.Time) string { return t.Format("2006-01-02 15:04") } func formatMonth(t time.Time) string { return t.Format("January 2006") } func daysInMonth(year int, month time.Month) int { return time.Date(year, month+1, 0, 0, 0, 0, 0, time.Local).Day() } func weekday(year int, month time.Month, day int) int { return int(time.Date(year, month, day, 0, 0, 0, 0, time.Local).Weekday()) } func add(a, b int) int { return a + b } func sub(a, b int) int { return a - b } func seq(start, end int) []int { s := make([]int, end-start+1) for i := range s { s[i] = start + i } return s } func isToday(year int, month time.Month, day int) bool { now := time.Now() return now.Year() == year && now.Month() == month && now.Day() == day } func isSameMonth(year int, month time.Month) bool { now := time.Now() return now.Year() == year && now.Month() == month }