package server import ( "context" "embed" "fmt" "io/fs" "net/http" "sync" "time" "git.mchus.pro/mchus/logpile/internal/collector" "git.mchus.pro/mchus/logpile/internal/models" ) // WebFS holds embedded web files (set by main package) var WebFS embed.FS type Config struct { Port int PreloadFile string AppVersion string AppCommit string } type Server struct { config Config mux *http.ServeMux httpServer *http.Server mu sync.RWMutex result *models.AnalysisResult detectedVendor string rawExport *RawExportPackage convertJobs map[string]struct{} convertOutput map[string]ConvertArtifact jobManager *JobManager collectors *collector.Registry } type ConvertArtifact struct { Path string Summary string } func New(cfg Config) *Server { s := &Server{ config: cfg, mux: http.NewServeMux(), jobManager: NewJobManager(), collectors: collector.NewDefaultRegistry(), convertJobs: make(map[string]struct{}), convertOutput: make(map[string]ConvertArtifact), } s.setupRoutes() return s } func (s *Server) setupRoutes() { // Static files staticContent, err := fs.Sub(WebFS, "static") if err != nil { panic(err) } s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticContent)))) // Pages s.mux.HandleFunc("/", s.handleIndex) // API endpoints s.mux.HandleFunc("POST /api/upload", s.handleUpload) s.mux.HandleFunc("GET /api/status", s.handleGetStatus) s.mux.HandleFunc("GET /api/parsers", s.handleGetParsers) s.mux.HandleFunc("GET /api/file-types", s.handleGetFileTypes) s.mux.HandleFunc("GET /api/events", s.handleGetEvents) s.mux.HandleFunc("GET /api/sensors", s.handleGetSensors) s.mux.HandleFunc("GET /api/config", s.handleGetConfig) s.mux.HandleFunc("GET /api/serials", s.handleGetSerials) s.mux.HandleFunc("GET /api/firmware", s.handleGetFirmware) s.mux.HandleFunc("GET /api/parse-errors", s.handleGetParseErrors) s.mux.HandleFunc("GET /api/export/csv", s.handleExportCSV) s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON) s.mux.HandleFunc("GET /api/export/reanimator", s.handleExportReanimator) s.mux.HandleFunc("POST /api/convert", s.handleConvertReanimatorBatch) s.mux.HandleFunc("GET /api/convert/{id}", s.handleConvertStatus) s.mux.HandleFunc("GET /api/convert/{id}/download", s.handleConvertDownload) s.mux.HandleFunc("DELETE /api/clear", s.handleClear) s.mux.HandleFunc("POST /api/shutdown", s.handleShutdown) s.mux.HandleFunc("POST /api/collect", s.handleCollectStart) s.mux.HandleFunc("GET /api/collect/{id}", s.handleCollectStatus) s.mux.HandleFunc("POST /api/collect/{id}/cancel", s.handleCollectCancel) } func (s *Server) Run() error { addr := fmt.Sprintf(":%d", s.config.Port) s.httpServer = &http.Server{ Addr: addr, Handler: s.mux, } return s.httpServer.ListenAndServe() } // Shutdown gracefully shuts down the server func (s *Server) Shutdown() { if s.httpServer != nil { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() s.httpServer.Shutdown(ctx) } } // SetResult sets the analysis result (thread-safe) func (s *Server) SetResult(result *models.AnalysisResult) { s.mu.Lock() defer s.mu.Unlock() s.result = result } // GetResult returns the analysis result (thread-safe) func (s *Server) GetResult() *models.AnalysisResult { s.mu.RLock() defer s.mu.RUnlock() return s.result } func (s *Server) SetRawExport(pkg *RawExportPackage) { s.mu.Lock() defer s.mu.Unlock() s.rawExport = pkg } func (s *Server) GetRawExport() *RawExportPackage { s.mu.RLock() defer s.mu.RUnlock() if s.rawExport == nil { return nil } cloned := *s.rawExport return &cloned } func (s *Server) ClientVersionString() string { s.mu.RLock() defer s.mu.RUnlock() v := s.config.AppVersion c := s.config.AppCommit if v == "" { v = "dev" } if c == "" { c = "none" } return fmt.Sprintf("LOGPile %s (commit: %s)", v, c) } // SetDetectedVendor sets the detected vendor name func (s *Server) SetDetectedVendor(vendor string) { s.mu.Lock() defer s.mu.Unlock() s.detectedVendor = vendor } // GetDetectedVendor returns the detected vendor name func (s *Server) GetDetectedVendor() string { s.mu.RLock() defer s.mu.RUnlock() return s.detectedVendor } func (s *Server) markConvertJob(id string) { s.mu.Lock() defer s.mu.Unlock() s.convertJobs[id] = struct{}{} } func (s *Server) isConvertJob(id string) bool { s.mu.RLock() defer s.mu.RUnlock() _, ok := s.convertJobs[id] return ok } func (s *Server) setConvertArtifact(id string, artifact ConvertArtifact) { s.mu.Lock() defer s.mu.Unlock() s.convertOutput[id] = artifact } func (s *Server) getConvertArtifact(id string) (ConvertArtifact, bool) { s.mu.RLock() defer s.mu.RUnlock() artifact, ok := s.convertOutput[id] return artifact, ok } func (s *Server) clearConvertArtifact(id string) { s.mu.Lock() defer s.mu.Unlock() delete(s.convertOutput, id) } func (s *Server) clearAllConvertArtifacts() []ConvertArtifact { s.mu.Lock() defer s.mu.Unlock() out := make([]ConvertArtifact, 0, len(s.convertOutput)) for _, artifact := range s.convertOutput { out = append(out, artifact) } s.convertOutput = make(map[string]ConvertArtifact) s.convertJobs = make(map[string]struct{}) return out }