package main import ( "context" "html/template" "net/http" "strings" "time" ) // webServer runs the web interface on port 8080 func webServer(shutdown chan struct{}) { // Load templates tmpl, err := template.ParseFiles("templates/index.html") if err != nil { logInfo("web", "Error loading templates", map[string]interface{}{ "error": err.Error(), }) return } // Define HTTP handlers http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { data, err := getDashboardData() if err != nil { http.Error(w, "Error fetching data: "+err.Error(), http.StatusInternalServerError) return } err = tmpl.Execute(w, data) if err != nil { // Check if it's already too late to write headers if !isResponseHeaderWritten(err) { http.Error(w, "Error rendering template: "+err.Error(), http.StatusInternalServerError) } else { // Log the error but don't try to write headers again logInfo("web", "Template execution error after headers sent", map[string]interface{}{ "error": err.Error(), }) } return } }) // Start the server server := &http.Server{ Addr: ":8080", Handler: nil, // Use default mux } // Create a goroutine for the server go func() { logInfo("web", "Starting web server", map[string]interface{}{ "port": 8080, }) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logInfo("web", "Web server error", map[string]interface{}{ "error": err.Error(), }) } }() // Wait for shutdown signal <-shutdown // Create a deadline for server shutdown ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Attempt graceful shutdown if err := server.Shutdown(ctx); err != nil { logInfo("web", "Error during server shutdown", map[string]interface{}{ "error": err.Error(), }) } logInfo("web", "Web server stopped", nil) } // getDashboardData fetches the data needed for the dashboard func getDashboardData() (DashboardData, error) { articles := loadArticles() now := time.Now() hourAgo := now.Add(-60 * time.Minute) // Prepare the data structure data := DashboardData{ LastUpdated: now.Format("Jan 02, 2006 15:04:05 MST"), TotalArticles: len(articles), } // Count broadcast articles, recent articles, and unsummarized articles for _, a := range articles { if !a.BroadcastTime.IsZero() && a.BroadcastTime.Unix() > 1 { data.TotalBroadcast++ } if a.FirstSeen.After(hourAgo) { data.NewInLastHour++ } if a.Summary == "" || a.Importance == 0 { data.UnsummarizedCount++ } } // Get broadcast history (last 100) history, err := getBroadcastHistory(100) if err != nil { return data, err } data.History = history // Get next up articles (importance sorted, less than 24 hours old) nextUp, err := getNextUpArticles() if err != nil { return data, err } data.NextUp = nextUp // Get recent logs recentLogs, err := getRecentLogs(100) if err != nil { logInfo("web", "Error fetching recent logs", map[string]interface{}{ "error": err.Error(), }) // Continue with empty logs list recentLogs = []LogEntry{} } data.RecentLogs = recentLogs return data, nil } // isResponseHeaderWritten checks if an error indicates headers were already written func isResponseHeaderWritten(err error) bool { // Check common patterns that indicate the response was already partially written errStr := err.Error() return strings.Contains(errStr, "write: broken pipe") || strings.Contains(errStr, "write: connection reset by peer") || strings.Contains(errStr, "http: superfluous response.WriteHeader") }