Add TFR warning banner for airspace restrictions
Fetches active Temporary Flight Restrictions from the FAA website, filters by configured state (LOCATION_STATE env var), and displays a red warning banner at the top of the dashboard when TFRs are present. Data is cached for 30 minutes and degrades gracefully if the FAA is unreachable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -78,6 +80,9 @@ func main() {
|
||||
Logger: logger,
|
||||
})
|
||||
|
||||
// Create FAA TFR client
|
||||
faaClient := client.NewFAAClient()
|
||||
|
||||
// Create HTTP server
|
||||
srv := server.New(cfg.Addr(), logger)
|
||||
|
||||
@@ -88,6 +93,7 @@ func main() {
|
||||
config: cfg,
|
||||
weatherSvc: weatherSvc,
|
||||
assessmentSvc: assessmentSvc,
|
||||
faaClient: faaClient,
|
||||
}
|
||||
srv.SetupRoutes(handler)
|
||||
|
||||
@@ -194,6 +200,12 @@ type Handler struct {
|
||||
config *config.Config
|
||||
weatherSvc *service.WeatherService
|
||||
assessmentSvc *service.AssessmentService
|
||||
faaClient *client.FAAClient
|
||||
|
||||
// TFR cache
|
||||
tfrCache []model.TFR
|
||||
tfrCacheAt time.Time
|
||||
tfrCacheMu sync.RWMutex
|
||||
}
|
||||
|
||||
// Health handles health check requests
|
||||
@@ -371,6 +383,63 @@ func (h *Handler) AssessConditions(w http.ResponseWriter, r *http.Request) {
|
||||
server.RespondJSON(w, 200, response)
|
||||
}
|
||||
|
||||
const tfrCacheTTL = 30 * time.Minute
|
||||
|
||||
// GetTFRs handles airspace TFR requests
|
||||
func (h *Handler) GetTFRs(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Check cache
|
||||
h.tfrCacheMu.RLock()
|
||||
if h.tfrCache != nil && time.Since(h.tfrCacheAt) < tfrCacheTTL {
|
||||
tfrs := h.tfrCache
|
||||
h.tfrCacheMu.RUnlock()
|
||||
server.RespondJSON(w, 200, map[string]interface{}{
|
||||
"tfrs": tfrs,
|
||||
"count": len(tfrs),
|
||||
"last_checked": h.tfrCacheAt.UTC(),
|
||||
})
|
||||
return
|
||||
}
|
||||
h.tfrCacheMu.RUnlock()
|
||||
|
||||
// Fetch from FAA
|
||||
allTfrs, err := h.faaClient.FetchTFRs(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to fetch TFRs", "error", err)
|
||||
// Return empty on error — don't show banner
|
||||
server.RespondJSON(w, 200, map[string]interface{}{
|
||||
"tfrs": []model.TFR{},
|
||||
"count": 0,
|
||||
"last_checked": time.Now().UTC(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Filter by configured state
|
||||
var filtered []model.TFR
|
||||
for _, tfr := range allTfrs {
|
||||
if strings.EqualFold(tfr.State, h.config.LocationState) {
|
||||
filtered = append(filtered, tfr)
|
||||
}
|
||||
}
|
||||
if filtered == nil {
|
||||
filtered = []model.TFR{}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
h.tfrCacheMu.Lock()
|
||||
h.tfrCache = filtered
|
||||
h.tfrCacheAt = time.Now()
|
||||
h.tfrCacheMu.Unlock()
|
||||
|
||||
server.RespondJSON(w, 200, map[string]interface{}{
|
||||
"tfrs": filtered,
|
||||
"count": len(filtered),
|
||||
"last_checked": time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
// WeatherFetcher runs background weather fetching
|
||||
type WeatherFetcher struct {
|
||||
logger *slog.Logger
|
||||
|
||||
Reference in New Issue
Block a user