package main import ( "flag" "log" "net/http" "os" "sync" "time" "zeitkort-web/repository" ) // Version is set at build time via -ldflags. var Version = "dev" const resetInterval = 10 * time.Minute // Server holds shared dependencies. type Server struct { mu sync.Mutex // guards Cfg/resetAt when Settings are saved/reset Cfg Config initialCfg Config resetAt time.Time repo repository.Repository info *log.Logger errl *log.Logger } func main() { cfgPath := flag.String("config", "config.json", "path to config.json") flag.Parse() info := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) errl := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) cfg, err := loadConfig(*cfgPath) if err != nil { errl.Fatalf("config: %v", err) } repo := repository.NewMemoryRepository() if err := repo.Migrate(); err != nil { errl.Fatalf("repository: %v", err) } s := &Server{ Cfg: cfg, initialCfg: cfg, resetAt: time.Now().Add(resetInterval), repo: repo, info: info, errl: errl, } if err := parseTemplates(); err != nil { errl.Fatalf("templates: %v", err) } go s.resetLoop(resetInterval) info.Printf("zeitkort-web %s listening on %s (web-only POC; in-memory state resets every %s)", Version, cfg.ListenAddr, resetInterval) if err := http.ListenAndServe(cfg.ListenAddr, s.routes()); err != nil { errl.Fatal(err) } } func (s *Server) nextResetAt() time.Time { s.mu.Lock() defer s.mu.Unlock() return s.resetAt } func (s *Server) resetLoop(interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { if resetter, ok := s.repo.(interface{ Reset() }); ok { resetter.Reset() } s.mu.Lock() s.Cfg = s.initialCfg s.resetAt = time.Now().Add(interval) s.mu.Unlock() s.info.Printf("in-memory proof-of-concept state reset after %s", interval) } } func (s *Server) routes() http.Handler { mux := http.NewServeMux() // Static assets (css + the one small timer script), embedded in the binary. mux.Handle("GET /static/", staticHandler()) // Main working area. mux.HandleFunc("GET /", s.handleIndex) mux.HandleFunc("GET /main", s.handleMain) // Companies. mux.HandleFunc("GET /companies/new", s.handleCompanyForm) mux.HandleFunc("GET /companies/{id}/edit", s.handleCompanyForm) mux.HandleFunc("POST /companies", s.handleCompanySave) mux.HandleFunc("POST /companies/{id}", s.handleCompanySave) mux.HandleFunc("POST /companies/{id}/delete", s.handleCompanyDelete) // Assignments. mux.HandleFunc("GET /assignments/new", s.handleAssignmentForm) mux.HandleFunc("GET /assignments/{id}/edit", s.handleAssignmentForm) mux.HandleFunc("POST /assignments", s.handleAssignmentSave) mux.HandleFunc("POST /assignments/{id}", s.handleAssignmentSave) mux.HandleFunc("POST /assignments/{id}/delete", s.handleAssignmentDelete) // Tasks. mux.HandleFunc("GET /tasks/new", s.handleTaskNewForm) mux.HandleFunc("GET /tasks/{id}/edit", s.handleTaskEditForm) mux.HandleFunc("POST /tasks", s.handleTaskCreate) mux.HandleFunc("POST /tasks/{id}", s.handleTaskUpdate) mux.HandleFunc("POST /tasks/{id}/delete", s.handleTaskDelete) // Work sessions. mux.HandleFunc("POST /sessions/start", s.handleSessionStart) mux.HandleFunc("POST /sessions/{id}/stop", s.handleSessionStop) mux.HandleFunc("GET /sessions/new", s.handleSessionForm) mux.HandleFunc("GET /sessions/{id}/edit", s.handleSessionForm) mux.HandleFunc("POST /sessions/manual", s.handleSessionSave) mux.HandleFunc("POST /sessions/{id}", s.handleSessionSave) mux.HandleFunc("POST /sessions/{id}/delete", s.handleSessionDelete) // Invoice. mux.HandleFunc("GET /invoice", s.handleInvoiceForm) mux.HandleFunc("GET /invoice/line", s.handleInvoiceLine) mux.HandleFunc("POST /invoice", s.handleInvoiceGenerate) // Settings and POC information. mux.HandleFunc("GET /settings", s.handleSettings) mux.HandleFunc("POST /settings", s.handleSettingsSave) mux.HandleFunc("GET /about", s.handleAbout) // Used by htmx to remove a row from the page (returns empty body). mux.HandleFunc("GET /void", func(w http.ResponseWriter, r *http.Request) {}) return s.logging(mux) } func (s *Server) logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) s.info.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond)) }) }