package server import ( "fmt" "html" "html/template" "io/fs" "log" "net/http" "os" "path/filepath" "strings" "sync" "time" webbundle "luxtools/web" ) const ( address = ":5000" htmlContentType = "text/html; charset=utf-8" notFoundTemplate = `
Error

Route %s not found.

` ) // Config contains the runtime options for the HTTP server. type Config struct { MediaDir string } // Server hosts the Luxtools HTTP handlers. type Server struct { mux *http.ServeMux templates *template.Template staticFS fs.FS mediaFS http.FileSystem counterMu sync.Mutex clickCount int } // New constructs a configured Server ready to serve requests. func New(cfg Config) (*Server, error) { if strings.TrimSpace(cfg.MediaDir) == "" { return nil, fmt.Errorf("media directory is required") } mediaDir := filepath.Clean(cfg.MediaDir) info, err := os.Stat(mediaDir) if err != nil { return nil, fmt.Errorf("media directory check failed: %w", err) } if !info.IsDir() { return nil, fmt.Errorf("media directory must be a directory: %s", mediaDir) } // Parse all templates tmpl, err := template.ParseFS(webbundle.Content, "templates/*.html") if err != nil { return nil, fmt.Errorf("failed to parse templates: %w", err) } staticFS, err := fs.Sub(webbundle.Content, "static") if err != nil { return nil, fmt.Errorf("failed to prepare static assets: %w", err) } s := &Server{ mux: http.NewServeMux(), templates: tmpl, staticFS: staticFS, mediaFS: http.Dir(mediaDir), } s.registerRoutes() return s, nil } // Router exposes the configured http.Handler for the server. func (s *Server) Router() http.Handler { return s.mux } // Address returns the default listen address for the server. func (s *Server) Address() string { return address } func (s *Server) registerRoutes() { staticHandler := http.FileServer(http.FS(s.staticFS)) mediaHandler := http.FileServer(s.mediaFS) s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(staticHandler))) s.mux.Handle("/media/", http.StripPrefix("/media/", allowGetHead(mediaHandler))) s.mux.HandleFunc("/", s.handleRequest) } func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": data := DefaultMenuBar() s.renderTemplate(w, http.StatusOK, "index.html", data) case "/admin": data := AdminMenuBar() data.Title = "Admin Panel" s.renderTemplate(w, http.StatusOK, "admin.html", data) case "/counter": s.handleCounter(w, r) case "/time": s.sendHTMLString(w, http.StatusOK, renderTime(time.Now())) default: s.sendHTMLString(w, http.StatusNotFound, fmt.Sprintf(notFoundTemplate, html.EscapeString(r.URL.Path))) } } func (s *Server) handleCounter(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost || r.Method == http.MethodPut { s.counterMu.Lock() s.clickCount++ s.counterMu.Unlock() } s.counterMu.Lock() count := s.clickCount s.counterMu.Unlock() s.sendHTMLString(w, http.StatusOK, renderCounter(count)) } func (s *Server) renderTemplate(w http.ResponseWriter, status int, name string, data interface{}) { w.Header().Set("Content-Type", htmlContentType) w.WriteHeader(status) if err := s.templates.ExecuteTemplate(w, name, data); err != nil { log.Printf("template execution failed: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func (s *Server) sendHTML(w http.ResponseWriter, status int, body []byte) { w.Header().Set("Content-Type", htmlContentType) w.WriteHeader(status) if len(body) > 0 { if _, err := w.Write(body); err != nil { log.Printf("write response failed: %v", err) } } } func (s *Server) sendHTMLString(w http.ResponseWriter, status int, body string) { s.sendHTML(w, status, []byte(body)) } func renderCounter(count int) string { return fmt.Sprintf(`

Clicks: %d

`, count) } func renderTime(now time.Time) string { return fmt.Sprintf(`

Server time: %s

`, now.Format("2006-01-02 15:04:05")) } func allowGetHead(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet, http.MethodHead: next.ServeHTTP(w, r) default: http.NotFound(w, r) } }) }