diff --git a/models.go b/models.go
index c20d474..fdac7a8 100644
--- a/models.go
+++ b/models.go
@@ -32,6 +32,8 @@ type DashboardData struct {
TotalBroadcast int
NewInLastHour int
UnsummarizedCount int
+ NextBroadcastIn string // Time until the next broadcast attempt
+ LastBroadcastTime time.Time // When the last broadcast occurred
NextUp []Article
History []Article
RecentLogs []LogEntry
diff --git a/templates/index.html b/templates/index.html
index 560b8c1..3309f2e 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -186,6 +186,10 @@
Awaiting Summary
{{.UnsummarizedCount}}
+
+
Next Broadcast In
+
{{.NextBroadcastIn}}
+
diff --git a/webserver.go b/webserver.go
index cf27653..07e5019 100644
--- a/webserver.go
+++ b/webserver.go
@@ -2,6 +2,7 @@ package main
import (
"context"
+ "fmt"
"html/template"
"net/http"
"strings"
@@ -19,8 +20,30 @@ func webServer(shutdown chan struct{}) {
return
}
- // Define HTTP handlers
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ // Create a custom request handler with security headers
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Add security headers
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+ w.Header().Set("X-Frame-Options", "DENY")
+ w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'")
+ w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
+ w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+
+ // Enforce request method
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Limit request body size (1MB)
+ r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
+
+ // Only serve the index page
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+
data, err := getDashboardData()
if err != nil {
http.Error(w, "Error fetching data: "+err.Error(), http.StatusInternalServerError)
@@ -42,10 +65,14 @@ func webServer(shutdown chan struct{}) {
}
})
- // Start the server
+ // Configure server with appropriate timeouts
server := &http.Server{
- Addr: ":8080",
- Handler: nil, // Use default mux
+ Addr: ":8080",
+ Handler: handler,
+ ReadTimeout: 10 * time.Second, // Time to read the request
+ WriteTimeout: 30 * time.Second, // Time to write the response
+ IdleTimeout: 60 * time.Second, // Keep-alive connections timeout
+ MaxHeaderBytes: 1 << 20, // 1MB max header size
}
// Create a goroutine for the server
@@ -91,9 +118,15 @@ func getDashboardData() (DashboardData, error) {
}
// Count broadcast articles, recent articles, and unsummarized articles
+ var lastBroadcastTime time.Time
for _, a := range articles {
if !a.BroadcastTime.IsZero() && a.BroadcastTime.Unix() > 1 {
data.TotalBroadcast++
+
+ // Track the most recent broadcast time
+ if a.BroadcastTime.After(lastBroadcastTime) {
+ lastBroadcastTime = a.BroadcastTime
+ }
}
if a.FirstSeen.After(hourAgo) {
@@ -105,6 +138,38 @@ func getDashboardData() (DashboardData, error) {
}
}
+ // Set the last broadcast time
+ data.LastBroadcastTime = lastBroadcastTime
+
+ // Calculate time until next broadcast
+ if lastBroadcastTime.IsZero() {
+ data.NextBroadcastIn = "As soon as articles are summarized"
+ } else {
+ nextBroadcastTime := lastBroadcastTime.Add(BROADCAST_INTERVAL)
+ if now.After(nextBroadcastTime) {
+ // If we're past the interval but haven't broadcast yet,
+ // likely waiting for articles to be summarized
+ if data.UnsummarizedCount > 0 {
+ data.NextBroadcastIn = "Waiting for articles to be summarized"
+ } else {
+ data.NextBroadcastIn = "Pending (checking every " + BROADCAST_CHECK_INTERVAL.String() + ")"
+ }
+ } else {
+ // We're still within the interval, calculate remaining time
+ timeUntilNextBroadcast := nextBroadcastTime.Sub(now)
+
+ // Format as hours and minutes
+ hours := int(timeUntilNextBroadcast.Hours())
+ minutes := int(timeUntilNextBroadcast.Minutes()) % 60
+
+ if hours > 0 {
+ data.NextBroadcastIn = fmt.Sprintf("%dh %dm", hours, minutes)
+ } else {
+ data.NextBroadcastIn = fmt.Sprintf("%dm", minutes)
+ }
+ }
+ }
+
// Get broadcast history (last 100)
history, err := getBroadcastHistory(100)
if err != nil {