Implementation Plan: Real-Time Aircraft Tracking Module

FlightAware AeroAPI Enterprise Integration


1. Executive Summary

Overview

The Real-Time Aircraft Tracking Module enables SkyBrief to provide live fleet monitoring, predictive arrival intelligence, and automated weather correlation for Part-CAT operators. This module integrates FlightAware AeroAPI Enterprise to track aircraft positions, receive flight status updates, and deliver proactive alerts based on actual vs. planned routes.

Key Features

Feature Description Value Proposition
Live Fleet Map Real-time aircraft positions on interactive Mapbox map Visual fleet awareness at a glance
Flight Status Tracking Live ETA updates, departure/arrival notifications Reduce dispatch workload, improve passenger comms
Weather Correlation Automatic weather checks every 5 minutes for active flights Proactive safety alerts, EASA compliance
Predictive Alerts Foresight ETA predictions with weather impact analysis Fuel planning, alternate decisions
Historical Tracks 12-month retention of flight paths for audit/analysis Regulatory compliance, trend analysis
Multi-tenancy Operator isolation with role-based access Data security, operational boundaries

Integration Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                         FLIGHTAWARE ENTERPRISE                           │
│                                                                          │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────────────┐  │
│  │  AeroAPI v4     │  │  Foresight      │  │  Push Alerts            │  │
│  │  (Positions)    │  │  (Predictions)  │  │  (Webhooks)             │  │
│  └────────┬────────┘  └────────┬────────┘  └───────────┬───────────────┘  │
│           │                    │                       │                  │
│           │ REST API           │ REST API              │ HTTPS POST       │
│           │ (Rate Limited)     │ (Rate Limited)        │ (HMAC Signed)    │
│           │                    │                       │                  │
└───────────┼────────────────────┼───────────────────────┼──────────────────┘
            │                    │                       │
            ▼                    ▼                       ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                      SKYBRIEF TRACKING MODULE                            │
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    ADAPTER LAYER                                  │   │
│  │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │   │
│  │  │ FlightAware     │  │ Webhook         │  │ Polling         │  │   │
│  │  │ Adapter         │  │ Handler         │  │ Service         │  │   │
│  │  └────────┬────────┘  └────────┬────────┘  └────────┬────────┘  │   │
│  └───────────┼────────────────────┼────────────────────┼───────────┘   │
│              │                    │                    │               │
│              ▼                    ▼                    ▼               │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                  APPLICATION LAYER                                │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐   │   │
│  │  │ Fleet        │  │ Flight       │  │ Alert                │   │   │
│  │  │ Monitor      │  │ Tracker      │  │ Correlator           │   │   │
│  │  └──────────────┘  └──────────────┘  └──────────────────────┘   │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    INFRASTRUCTURE                                 │   │
│  │  PostgreSQL  │  Redis  │  WebSocket  │  Mapbox GL JS            │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

Value Proposition

For Operations Managers: - Monitor entire fleet from single dashboard - Proactive alerts for diversions, delays, weather issues - Automated EASA compliance documentation with live position data

For Chief Pilots: - Real-time flight following for duty time monitoring - Weather correlation with actual flight progress - Post-flight analysis with accurate track data

For Dispatchers: - Reduce manual flight tracking by 80% - Automatic ETA updates to passengers/FBOs - Integration with existing flight planning workflow


2. Architecture Overview

Hexagonal Architecture Extension

The tracking module extends the existing hexagonal architecture with three new ports and corresponding adapters:

┌─────────────────────────────────────────────────────────────┐
│                      EXISTING SYSTEM                         │
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐       │
│  │   Weather    │  │  Compliance  │  │   Alerts     │       │
│  │   Service    │  │   Service    │  │   Service    │       │
│  └──────────────┘  └──────────────┘  └──────────────┘       │
│                                                              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼ Integration Points
┌─────────────────────────────────────────────────────────────┐
│                    NEW TRACKING MODULE                       │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │  DOMAIN LAYER                                           ││
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ││
│  │  │   Aircraft   │  │   Flight     │  │   Position   │  ││
│  │  │   Position   │  │   Track      │  │   Alert      │  ││
│  │  └──────────────┘  └──────────────┘  └──────────────┘  ││
│  └─────────────────────────────────────────────────────────┘│
│                            │                                 │
│  ┌─────────────────────────▼─────────────────────────────────┐│
│  │  APPLICATION LAYER                                        ││
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    ││
│  │  │ Fleet Monitor│  │Track Flights │  │Weather Alert│    ││
│  │  │   Service    │  │   Service    │  │ Correlator  │    ││
│  │  └──────────────┘  └──────────────┘  └──────────────┘    ││
│  └─────────────────────────────────────────────────────────┘│
│                            │                                 │
│  ┌─────────────────────────▼─────────────────────────────────┐│
│  │  PORTS (Interfaces)                                       ││
│  │  ┌──────────────────────┐  ┌──────────────────────┐      ││
│  │  │ FlightTrackingService│  │ AircraftPositionRepo │      ││
│  │  │    (NEW)             │  │     (NEW)            │      ││
│  │  └──────────────────────┘  └──────────────────────┘      ││
│  └─────────────────────────────────────────────────────────┘│
│                            │                                 │
│  ┌─────────────────────────▼─────────────────────────────────┐│
│  │  ADAPTERS                                                 ││
│  │  ┌──────────────────┐  ┌──────────────────┐              ││
│  │  │ FlightAware      │  │ PostgreSQL       │              ││
│  │  │ Adapter          │  │ Position Repo    │              ││
│  │  └──────────────────┘  └──────────────────┘              ││
│  │  ┌──────────────────┐  ┌──────────────────┐              ││
│  │  │ Webhook Handler  │  │ Polling Service  │              ││
│  │  └──────────────────┘  └──────────────────┘              ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

Data Flow Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                         DATA FLOW - ACTIVE FLIGHT                        │
└─────────────────────────────────────────────────────────────────────────┘

1. FLIGHT CREATION
   ┌─────────┐     POST /api/v1/flights      ┌─────────┐
   │  User   │ ──────────────────────────────▶ │  API    │
   │(Web App)│                                 │ Handler │
   └─────────┘                                 └────┬────┘
                                                    │
                                                    ▼
                                            ┌───────────────┐
                                            │ Flight Created│
                                            │ Status: PLANNED│
                                            └───────┬───────┘
                                                    │
                                                    ▼
                                            ┌───────────────┐
                                            │ Start Polling │
                                            │ (60s interval)│
                                            └───────┬───────┘

2. POSITION POLLING
   ┌───────────────┐     GET /flights/{faFlightId}    ┌─────────────────┐
   │ Polling       │ ─────────────────────────────────▶ │ FlightAware     │
   │ Service       │                                    │ AeroAPI         │
   │ (60s)         │ ◀───────────────────────────────── │                 │
   └───────┬───────┘         Position + Track           └─────────────────┘
           │
           ▼
   ┌───────────────┐
   │ Cache in Redis│
   │ (TTL: 5 min)  │
   └───────┬───────┘
           │
           ▼
   ┌───────────────┐     INSERT/UPDATE     ┌───────────────┐
   │ Position Data │ ─────────────────────▶ │ PostgreSQL    │
   │ (Parsed)      │                        │ aircraft_pos  │
   └───────────────┘                        └───────────────┘
           │
           │ Every 5 min
           ▼
   ┌───────────────┐
   │ Weather Check │
   │ Correlation   │
   └───────────────┘

3. WEBHOOK ALERTS (Real-time)
   ┌─────────────────┐     POST /webhooks/flightaware    ┌───────────────┐
   │ FlightAware     │ ─────────────────────────────────▶ │ Webhook       │
   │ Push Service    │         HMAC Signed                │ Handler       │
   │                 │                                    │               │
   └─────────────────┘                                    └───────┬───────┘
                                                                  │
                                                                  ▼
                                                          ┌───────────────┐
                                                          │ Validate      │
                                                          │ Signature     │
                                                          └───────┬───────┘
                                                                  │
                                                                  ▼
                                                          ┌───────────────┐
                                                          │ Process Alert │
                                                          │ (diversion,   │
                                                          │ arrival, etc) │
                                                          └───────┬───────┘
                                                                  │
                                        ┌──────────────────────────┼──────────────────────────┐
                                        │                          │                          │
                                        ▼                          ▼                          ▼
                                ┌───────────────┐        ┌───────────────┐        ┌───────────────┐
                                │ Save to DB    │        │ WebSocket     │        │ Send Email    │
                                │ (audit trail) │        │ Broadcast     │        │ Notification  │
                                └───────────────┘        └───────────────┘        └───────────────┘

4. FRONTEND DISPLAY
   ┌───────────────┐        WebSocket /ws/fleet-updates      ┌───────────────┐
   │ React App     │ ◀────────────────────────────────────── │ WebSocket     │
   │ (Mapbox GL)   │           Real-time updates             │ Server        │
   └───────┬───────┘                                         └───────────────┘
           │
           │ GET /api/v1/fleet (initial load)
           ▼
   ┌───────────────┐
   │ Mapbox Map    │
   │ - Aircraft    │
   │   markers     │
   │ - Weather     │
   │   overlay     │
   │ - Flight      │
   │   paths       │
   └───────────────┘

New Components Overview

Component Type Responsibility File Path
FlightTrackingService Port Interface for tracking operations internal/ports/tracking.go
AircraftPositionRepo Port Repository for position data internal/ports/tracking.go
FlightAwareAdapter Adapter FlightAware API integration internal/adapters/tracking/flightaware.go
PositionRepository Adapter PostgreSQL position storage internal/adapters/persistence/postgres/position_repo.go
WebhookHandler Adapter HTTP webhook endpoint internal/adapters/http/handlers/webhooks.go
TrackingPoller Service Background polling job internal/application/tracking/poller.go
FleetDashboard Component React map component frontend/src/components/fleet/FleetDashboard.tsx

3. Backend Implementation

A. New Port Interfaces (internal/ports/tracking.go)

package ports

import (
    "context"
    "time"

    "github.com/google/uuid"
)

// ============================================================================
// DOMAIN TYPES
// ============================================================================

// AircraftPosition represents a single position report for an aircraft
type AircraftPosition struct {
    ID            uuid.UUID `json:"id"`
    FlightID      uuid.UUID `json:"flight_id"`
    AircraftID    uuid.UUID `json:"aircraft_id"`
    OperatorID    uuid.UUID `json:"operator_id"`

    // Position data
    Latitude      float64   `json:"latitude"`
    Longitude     float64   `json:"longitude"`
    Altitude      int       `json:"altitude"`       // feet MSL
    GroundSpeed   int       `json:"ground_speed"`   // knots
    Track         int       `json:"track"`          // degrees true
    VerticalSpeed int       `json:"vertical_speed"` // feet per minute

    // Source data
    Source        string    `json:"source"`         // "flightaware", "adsb", etc.
    Timestamp     time.Time `json:"timestamp"`      // Position timestamp
    ReceivedAt    time.Time `json:"received_at"`    // When we received it

    // FlightAware specific
    FAFlightID    string    `json:"fa_flight_id,omitempty"`
}

// FlightTrack represents a series of positions for a flight
type FlightTrack struct {
    ID            uuid.UUID           `json:"id"`
    FlightID      uuid.UUID           `json:"flight_id"`
    AircraftID    uuid.UUID           `json:"aircraft_id"`
    OperatorID    uuid.UUID           `json:"operator_id"`

    // Track metadata
    StartTime     time.Time           `json:"start_time"`
    EndTime       *time.Time          `json:"end_time,omitempty"`
    PointCount    int                 `json:"point_count"`

    // FlightAware specific
    FAFlightID    string              `json:"fa_flight_id"`

    // Positions (loaded on demand or paginated)
    Positions     []AircraftPosition  `json:"positions,omitempty"`
}

// FlightStatus represents the current status of a tracked flight
type FlightStatus string

const (
    FlightStatusPlanned    FlightStatus = "planned"
    FlightStatusScheduled  FlightStatus = "scheduled"
    FlightStatusActive     FlightStatus = "active"
    FlightStatusArrived    FlightStatus = "arrived"
    FlightStatusDiverted   FlightStatus = "diverted"
    FlightStatusCancelled  FlightStatus = "cancelled"
    FlightStatusDelayed    FlightStatus = "delayed"
)

// TrackedFlight represents a flight being actively tracked
type TrackedFlight struct {
    ID              uuid.UUID     `json:"id"`
    FlightID        uuid.UUID     `json:"flight_id"`
    AircraftID      uuid.UUID     `json:"aircraft_id"`
    OperatorID      uuid.UUID     `json:"operator_id"`

    // Flight details
    Callsign        string        `json:"callsign"`
    Registration    string        `json:"registration"`
    Origin          string        `json:"origin"`        // ICAO
    Destination     string        `json:"destination"`   // ICAO

    // Timing
    ScheduledOut    *time.Time    `json:"scheduled_out,omitempty"`
    ScheduledIn     *time.Time    `json:"scheduled_in,omitempty"`
    ActualOut       *time.Time    `json:"actual_out,omitempty"`
    ActualIn        *time.Time    `json:"actual_in,omitempty"`
    EstimatedIn     *time.Time    `json:"estimated_in,omitempty"`

    // Status
    Status          FlightStatus  `json:"status"`

    // Current position (if active)
    CurrentPosition *AircraftPosition `json:"current_position,omitempty"`

    // FlightAware specific
    FAFlightID      string        `json:"fa_flight_id"`

    // Tracking metadata
    LastUpdated     time.Time     `json:"last_updated"`
    IsTracking      bool          `json:"is_tracking"`
}

// TrackingAlertType represents types of tracking-related alerts
type TrackingAlertType string

const (
    AlertTypeDiversion      TrackingAlertType = "diversion"
    AlertTypeArrival        TrackingAlertType = "arrival"
    AlertTypeDeparture      TrackingAlertType = "departure"
    AlertTypeCancellation   TrackingAlertType = "cancellation"
    AlertTypeDelay          TrackingAlertType = "delay"
    AlertTypeWeatherEnRoute TrackingAlertType = "weather_enroute"
    AlertTypeETAChange      TrackingAlertType = "eta_change"
)

// TrackingAlert represents an alert generated from flight tracking
type TrackingAlert struct {
    ID              uuid.UUID         `json:"id"`
    FlightID        uuid.UUID         `json:"flight_id"`
    AircraftID      uuid.UUID         `json:"aircraft_id"`
    OperatorID      uuid.UUID         `json:"operator_id"`

    // Alert details
    Type            TrackingAlertType `json:"type"`
    Severity        string            `json:"severity"` // low, medium, high, critical
    Title           string            `json:"title"`
    Message         string            `json:"message"`

    // Related data
    PreviousValue   *string           `json:"previous_value,omitempty"`
    CurrentValue    *string           `json:"current_value,omitempty"`

    // Acknowledgment
    AcknowledgedAt  *time.Time        `json:"acknowledged_at,omitempty"`
    AcknowledgedBy  *uuid.UUID        `json:"acknowledged_by,omitempty"`

    // Metadata
    CreatedAt       time.Time         `json:"created_at"`
    Source          string            `json:"source"` // "flightaware_webhook", "weather_correlation", etc.
}

// ============================================================================
// PORT INTERFACES
// ============================================================================

// FlightTrackingService defines operations for tracking aircraft
type FlightTrackingService interface {
    // Position operations
    GetCurrentPosition(ctx context.Context, flightID uuid.UUID) (*AircraftPosition, error)
    GetFlightTrack(ctx context.Context, flightID uuid.UUID, start, end time.Time) (*FlightTrack, error)

    // Flight operations
    GetTrackedFlight(ctx context.Context, flightID uuid.UUID) (*TrackedFlight, error)
    ListTrackedFlights(ctx context.Context, operatorID uuid.UUID, status *FlightStatus) ([]TrackedFlight, error)

    // Tracking control
    StartTracking(ctx context.Context, flightID uuid.UUID, faFlightID string) error
    StopTracking(ctx context.Context, flightID uuid.UUID) error

    // Foresight predictions (FlightAware premium feature)
    GetForesightPrediction(ctx context.Context, flightID uuid.UUID) (*ForesightPrediction, error)

    // Alert operations
    SetAlert(ctx context.Context, alert *TrackingAlert) error
    AcknowledgeAlert(ctx context.Context, alertID, userID uuid.UUID) error
    ListAlerts(ctx context.Context, operatorID uuid.UUID, unacknowledgedOnly bool) ([]TrackingAlert, error)
}

// ForesightPrediction represents FlightAware's ETA prediction
type ForesightPrediction struct {
    FlightID        uuid.UUID     `json:"flight_id"`
    PredictedOut    *time.Time    `json:"predicted_out,omitempty"`
    PredictedOff    *time.Time    `json:"predicted_off,omitempty"`
    PredictedOn     *time.Time    `json:"predicted_on,omitempty"`
    PredictedIn     *time.Time    `json:"predicted_in,omitempty"`
    PredictedETA    time.Time     `json:"predicted_eta"`
    Confidence      float64       `json:"confidence"` // 0.0 - 1.0
    Trend           string        `json:"trend"`      // "early", "late", "on_time"
    MinutesOffset   int           `json:"minutes_offset"`
}

// AircraftPositionRepository defines storage operations for position data
type AircraftPositionRepository interface {
    SavePosition(ctx context.Context, position *AircraftPosition) error
    SavePositions(ctx context.Context, positions []AircraftPosition) error

    GetLatestPosition(ctx context.Context, aircraftID uuid.UUID) (*AircraftPosition, error)
    GetPositionsForFlight(ctx context.Context, flightID uuid.UUID, start, end time.Time) ([]AircraftPosition, error)
    GetPositionsForOperator(ctx context.Context, operatorID uuid.UUID) ([]AircraftPosition, error)

    // Cleanup (TTL enforcement)
    DeleteOldPositions(ctx context.Context, before time.Time) (int64, error)
}

// TrackingAlertRepository defines storage operations for tracking alerts
type TrackingAlertRepository interface {
    SaveAlert(ctx context.Context, alert *TrackingAlert) error
    GetAlert(ctx context.Context, alertID uuid.UUID) (*TrackingAlert, error)
    ListAlerts(ctx context.Context, operatorID uuid.UUID, options AlertListOptions) ([]TrackingAlert, error)
    AcknowledgeAlert(ctx context.Context, alertID, userID uuid.UUID) error

    // Cleanup
    DeleteOldAlerts(ctx context.Context, before time.Time) (int64, error)
}

// AlertListOptions provides filtering options for alert queries
type AlertListOptions struct {
    FlightID          *uuid.UUID
    Types             []TrackingAlertType
    Severities        []string
    UnacknowledgedOnly bool
    Limit             int
    Offset            int
}

// ExternalTrackingProvider defines the interface for external tracking services
type ExternalTrackingProvider interface {
    // Flight operations
    GetFlightPosition(ctx context.Context, faFlightID string) (*ProviderPosition, error)
    GetFlightTrack(ctx context.Context, faFlightID string) (*ProviderTrack, error)
    GetForesightPrediction(ctx context.Context, faFlightID string) (*ProviderForesight, error)

    // Search operations
    SearchFlight(ctx context.Context, callsign string, date time.Time) ([]FlightSearchResult, error)
    GetFlightByID(ctx context.Context, faFlightID string) (*FlightInfo, error)

    // Alert configuration
    SetAlert(ctx context.Context, faFlightID string, alertType string, config AlertConfig) error
    DeleteAlert(ctx context.Context, faFlightID string, alertType string) error

    // Webhook handling
    VerifyWebhookSignature(ctx context.Context, payload []byte, signature string) error
    ParseWebhookPayload(ctx context.Context, payload []byte) (*WebhookEvent, error)
}

// ProviderPosition represents position data from external provider
type ProviderPosition struct {
    FAFlightID      string    `json:"fa_flight_id"`
    Callsign        string    `json:"callsign"`
    Registration    string    `json:"registration"`

    Latitude        float64   `json:"latitude"`
    Longitude       float64   `json:"longitude"`
    Altitude        int       `json:"altitude"`
    GroundSpeed     int       `json:"ground_speed"`
    Track           int       `json:"track"`
    VerticalSpeed   int       `json:"vertical_speed"`

    Timestamp       time.Time `json:"timestamp"`
}

// ProviderTrack represents track data from external provider
type ProviderTrack struct {
    FAFlightID      string               `json:"fa_flight_id"`
    Positions       []ProviderPosition   `json:"positions"`
}

// ProviderForesight represents Foresight data from FlightAware
type ProviderForesight struct {
    FAFlightID      string     `json:"fa_flight_id"`
    PredictedETA    time.Time  `json:"predicted_eta"`
    Confidence      float64    `json:"confidence"`
    Trend           string     `json:"trend"`
    MinutesOffset   int        `json:"minutes_offset"`
}

// FlightSearchResult represents a flight search result
type FlightSearchResult struct {
    FAFlightID      string     `json:"fa_flight_id"`
    Callsign        string     `json:"callsign"`
    Registration    string     `json:"registration"`
    Origin          string     `json:"origin"`
    Destination     string     `json:"destination"`
    ScheduledOut    *time.Time `json:"scheduled_out,omitempty"`
}

// FlightInfo represents detailed flight information
type FlightInfo struct {
    FAFlightID      string         `json:"fa_flight_id"`
    Callsign        string         `json:"callsign"`
    Registration    string         `json:"registration"`
    Origin          string         `json:"origin"`
    Destination     string         `json:"destination"`
    AircraftType    string         `json:"aircraft_type"`

    ScheduledOut    *time.Time     `json:"scheduled_out,omitempty"`
    ScheduledIn     *time.Time     `json:"scheduled_in,omitempty"`
    ActualOut       *time.Time     `json:"actual_out,omitempty"`
    ActualIn        *time.Time     `json:"actual_in,omitempty"`
    EstimatedIn     *time.Time     `json:"estimated_in,omitempty"`

    Status          string         `json:"status"`
    ProgressPercent int            `json:"progress_percent"`
}

// AlertConfig represents configuration for external alerts
type AlertConfig struct {
    Enabled         bool              `json:"enabled"`
    Channels        []string          `json:"channels"` // "webhook", "email", etc.
    Conditions      map[string]interface{} `json:"conditions,omitempty"`
}

// WebhookEvent represents a parsed webhook event
type WebhookEvent struct {
    ID              string                 `json:"id"`
    Type            string                 `json:"type"` // "flight", "position", etc.
    EventType       string                 `json:"event_type"` // "departure", "arrival", etc.
    Timestamp       time.Time              `json:"timestamp"`
    FAFlightID      string                 `json:"fa_flight_id"`
    Payload         map[string]interface{} `json:"payload"`
}

B. FlightAware Adapter (internal/adapters/tracking/flightaware.go)

package tracking

import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "strconv"
    "time"

    "golang.org/x/time/rate"

    "github.com/skybrief/internal/ports"
)

const (
    defaultBaseURL   = "https://aeroapi.flightaware.com/aeroapi"
    defaultRateLimit = 100 // requests per minute (Enterprise tier)
)

// FlightAwareAdapter implements ExternalTrackingProvider for FlightAware AeroAPI
type FlightAwareAdapter struct {
    apiKey      string
    apiSecret   string // For webhook signature verification
    baseURL     string
    client      *http.Client
    rateLimiter *rate.Limiter

    // Caching
    cache       Cache // Redis or in-memory
}

// Config holds configuration for the FlightAware adapter
type Config struct {
    APIKey      string
    APISecret   string
    BaseURL     string
    RateLimit   int // requests per minute
    Timeout     time.Duration
}

// NewFlightAwareAdapter creates a new FlightAware adapter instance
func NewFlightAwareAdapter(cfg Config) (*FlightAwareAdapter, error) {
    if cfg.APIKey == "" {
        return nil, fmt.Errorf("flightaware API key is required")
    }

    if cfg.BaseURL == "" {
        cfg.BaseURL = defaultBaseURL
    }

    if cfg.RateLimit == 0 {
        cfg.RateLimit = defaultRateLimit
    }

    if cfg.Timeout == 0 {
        cfg.Timeout = 30 * time.Second
    }

    limiter := rate.NewLimiter(rate.Limit(cfg.RateLimit)/60, cfg.RateLimit)

    return &FlightAwareAdapter{
        apiKey:      cfg.APIKey,
        apiSecret:   cfg.APISecret,
        baseURL:     cfg.BaseURL,
        client:      &http.Client{Timeout: cfg.Timeout},
        rateLimiter: limiter,
    }, nil
}

// GetFlightPosition retrieves current position for a flight
func (a *FlightAwareAdapter) GetFlightPosition(ctx context.Context, faFlightID string) (*ports.ProviderPosition, error) {
    if err := a.rateLimiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limit exceeded: %w", err)
    }

    endpoint := fmt.Sprintf("%s/flights/%s/position", a.baseURL, url.PathEscape(faFlightID))

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("x-apikey", a.apiKey)
    req.Header.Set("Accept", "application/json")

    resp, err := a.client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("flightaware API error: status %d", resp.StatusCode)
    }

    var result struct {
        Position struct {
            FaFlightID    string  `json:"fa_flight_id"`
            Callsign      string  `json:"callsign"`
            Registration  string  `json:"registration"`
            Latitude      float64 `json:"latitude"`
            Longitude     float64 `json:"longitude"`
            Altitude      int     `json:"altitude"`
            GroundSpeed   int     `json:"groundspeed"`
            Track         int     `json:"heading"`
            Timestamp     int64   `json:"timestamp"`
        } `json:"position"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decoding response: %w", err)
    }

    return &ports.ProviderPosition{
        FAFlightID:   result.Position.FaFlightID,
        Callsign:     result.Position.Callsign,
        Registration: result.Position.Registration,
        Latitude:     result.Position.Latitude,
        Longitude:    result.Position.Longitude,
        Altitude:     result.Position.Altitude,
        GroundSpeed:  result.Position.GroundSpeed,
        Track:        result.Position.Track,
        Timestamp:    time.Unix(result.Position.Timestamp, 0),
    }, nil
}

// GetFlightTrack retrieves the full track for a flight
func (a *FlightAwareAdapter) GetFlightTrack(ctx context.Context, faFlightID string) (*ports.ProviderTrack, error) {
    if err := a.rateLimiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limit exceeded: %w", err)
    }

    endpoint := fmt.Sprintf("%s/flights/%s/track", a.baseURL, url.PathEscape(faFlightID))

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("x-apikey", a.apiKey)
    req.Header.Set("Accept", "application/json")

    resp, err := a.client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("flightaware API error: status %d", resp.StatusCode)
    }

    var result struct {
        Track []struct {
            Timestamp   int64   `json:"timestamp"`
            Latitude    float64 `json:"latitude"`
            Longitude   float64 `json:"longitude"`
            Altitude    int     `json:"altitude"`
            GroundSpeed int     `json:"groundspeed"`
            Heading     int     `json:"heading"`
        } `json:"track"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decoding response: %w", err)
    }

    positions := make([]ports.ProviderPosition, len(result.Track))
    for i, point := range result.Track {
        positions[i] = ports.ProviderPosition{
            Latitude:    point.Latitude,
            Longitude:   point.Longitude,
            Altitude:    point.Altitude,
            GroundSpeed: point.GroundSpeed,
            Track:       point.Heading,
            Timestamp:   time.Unix(point.Timestamp, 0),
        }
    }

    return &ports.ProviderTrack{
        FAFlightID: faFlightID,
        Positions:  positions,
    }, nil
}

// GetForesightPrediction retrieves Foresight ETA prediction
func (a *FlightAwareAdapter) GetForesightPrediction(ctx context.Context, faFlightID string) (*ports.ProviderForesight, error) {
    if err := a.rateLimiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limit exceeded: %w", err)
    }

    endpoint := fmt.Sprintf("%s/flights/%s/foresight", a.baseURL, url.PathEscape(faFlightID))

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("x-apikey", a.apiKey)
    req.Header.Set("Accept", "application/json")

    resp, err := a.client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode == http.StatusNotFound {
        // Foresight not available for this flight
        return nil, nil
    }

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("flightaware API error: status %d", resp.StatusCode)
    }

    var result struct {
        Foresight struct {
            PredictedETA  int64   `json:"predicted_arrival_time"`
            Confidence    float64 `json:"confidence"`
            Trend         string  `json:"trend"`
            MinutesOffset int     `json:"minutes_offset"`
        } `json:"foresight"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decoding response: %w", err)
    }

    return &ports.ProviderForesight{
        FAFlightID:    faFlightID,
        PredictedETA:  time.Unix(result.Foresight.PredictedETA, 0),
        Confidence:    result.Foresight.Confidence,
        Trend:         result.Foresight.Trend,
        MinutesOffset: result.Foresight.MinutesOffset,
    }, nil
}

// SearchFlight searches for flights by callsign
func (a *FlightAwareAdapter) SearchFlight(ctx context.Context, callsign string, date time.Time) ([]ports.FlightSearchResult, error) {
    if err := a.rateLimiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limit exceeded: %w", err)
    }

    endpoint := fmt.Sprintf("%s/flights/search", a.baseURL)

    query := url.Values{}
    query.Set("query", fmt.Sprintf("-callsign %s", callsign))
    query.Set("start", date.Format("2006-01-02"))
    query.Set("end", date.Add(24*time.Hour).Format("2006-01-02"))
    query.Set("max_pages", "1")

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint+"?"+query.Encode(), nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("x-apikey", a.apiKey)
    req.Header.Set("Accept", "application/json")

    resp, err := a.client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("flightaware API error: status %d", resp.StatusCode)
    }

    var result struct {
        Flights []struct {
            FaFlightID      string `json:"fa_flight_id"`
            Callsign        string `json:"callsign"`
            Registration    string `json:"registration"`
            Origin          struct {
                Code string `json:"code"`
            } `json:"origin"`
            Destination     struct {
                Code string `json:"code"`
            } `json:"destination"`
            ScheduledOut    int64  `json:"scheduled_out"`
        } `json:"flights"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decoding response: %w", err)
    }

    results := make([]ports.FlightSearchResult, len(result.Flights))
    for i, flight := range result.Flights {
        results[i] = ports.FlightSearchResult{
            FAFlightID:   flight.FaFlightID,
            Callsign:     flight.Callsign,
            Registration: flight.Registration,
            Origin:       flight.Origin.Code,
            Destination:  flight.Destination.Code,
        }
        if flight.ScheduledOut > 0 {
            t := time.Unix(flight.ScheduledOut, 0)
            results[i].ScheduledOut = &t
        }
    }

    return results, nil
}

// GetFlightByID retrieves detailed flight information
func (a *FlightAwareAdapter) GetFlightByID(ctx context.Context, faFlightID string) (*ports.FlightInfo, error) {
    if err := a.rateLimiter.Wait(ctx); err != nil {
        return nil, fmt.Errorf("rate limit exceeded: %w", err)
    }

    endpoint := fmt.Sprintf("%s/flights/%s", a.baseURL, url.PathEscape(faFlightID))

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("x-apikey", a.apiKey)
    req.Header.Set("Accept", "application/json")

    resp, err := a.client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("flightaware API error: status %d", resp.StatusCode)
    }

    var result struct {
        Flight struct {
            FaFlightID      string `json:"fa_flight_id"`
            Callsign        string `json:"callsign"`
            Registration    string `json:"registration"`
            AircraftType    string `json:"aircraft_type"`
            Origin          struct {
                Code string `json:"code"`
            } `json:"origin"`
            Destination     struct {
                Code string `json:"code"`
            } `json:"destination"`
            ScheduledOut    int64  `json:"scheduled_out"`
            ScheduledIn     int64  `json:"scheduled_in"`
            ActualOut       int64  `json:"actual_out"`
            ActualIn        int64  `json:"actual_in"`
            EstimatedIn     int64  `json:"estimated_in"`
            Status          string `json:"status"`
            ProgressPercent int    `json:"progress_percent"`
        } `json:"flight"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decoding response: %w", err)
    }

    info := &ports.FlightInfo{
        FAFlightID:      result.Flight.FaFlightID,
        Callsign:        result.Flight.Callsign,
        Registration:    result.Flight.Registration,
        AircraftType:    result.Flight.AircraftType,
        Origin:          result.Flight.Origin.Code,
        Destination:     result.Flight.Destination.Code,
        Status:          result.Flight.Status,
        ProgressPercent: result.Flight.ProgressPercent,
    }

    if result.Flight.ScheduledOut > 0 {
        t := time.Unix(result.Flight.ScheduledOut, 0)
        info.ScheduledOut = &t
    }
    if result.Flight.ScheduledIn > 0 {
        t := time.Unix(result.Flight.ScheduledIn, 0)
        info.ScheduledIn = &t
    }
    if result.Flight.ActualOut > 0 {
        t := time.Unix(result.Flight.ActualOut, 0)
        info.ActualOut = &t
    }
    if result.Flight.ActualIn > 0 {
        t := time.Unix(result.Flight.ActualIn, 0)
        info.ActualIn = &t
    }
    if result.Flight.EstimatedIn > 0 {
        t := time.Unix(result.Flight.EstimatedIn, 0)
        info.EstimatedIn = &t
    }

    return info, nil
}

// SetAlert configures alerts for a flight
func (a *FlightAwareAdapter) SetAlert(ctx context.Context, faFlightID string, alertType string, config ports.AlertConfig) error {
    // FlightAware Push Alerts configuration via API
    // Note: This may require additional setup in FlightAware dashboard

    endpoint := fmt.Sprintf("%s/alerts", a.baseURL)

    body := map[string]interface{}{
        "fa_flight_id": faFlightID,
        "event_type":   alertType,
        "enabled":      config.Enabled,
        "channels":     config.Channels,
    }

    jsonBody, err := json.Marshal(body)
    if err != nil {
        return fmt.Errorf("marshaling request body: %w", err)
    }

    req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonBody))
    if err != nil {
        return fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("x-apikey", a.apiKey)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Accept", "application/json")

    resp, err := a.client.Do(req)
    if err != nil {
        return fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
        return fmt.Errorf("flightaware API error: status %d", resp.StatusCode)
    }

    return nil
}

// DeleteAlert removes an alert configuration
func (a *FlightAwareAdapter) DeleteAlert(ctx context.Context, faFlightID string, alertType string) error {
    endpoint := fmt.Sprintf("%s/alerts/%s/%s", a.baseURL, url.PathEscape(faFlightID), alertType)

    req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
    if err != nil {
        return fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("x-apikey", a.apiKey)

    resp, err := a.client.Do(req)
    if err != nil {
        return fmt.Errorf("executing request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
        return fmt.Errorf("flightaware API error: status %d", resp.StatusCode)
    }

    return nil
}

// VerifyWebhookSignature validates webhook HMAC signature
func (a *FlightAwareAdapter) VerifyWebhookSignature(ctx context.Context, payload []byte, signature string) error {
    if a.apiSecret == "" {
        return fmt.Errorf("webhook secret not configured")
    }

    // Expected signature format: "sha256=<hex-encoded-hmac>"
    const prefix = "sha256="
    if !strings.HasPrefix(signature, prefix) {
        return fmt.Errorf("invalid signature format")
    }

    expectedSig := signature[len(prefix):]

    // Calculate HMAC
    mac := hmac.New(sha256.New, []byte(a.apiSecret))
    mac.Write(payload)
    computedSig := hex.EncodeToString(mac.Sum(nil))

    // Constant-time comparison
    if !hmac.Equal([]byte(expectedSig), []byte(computedSig)) {
        return fmt.Errorf("invalid webhook signature")
    }

    return nil
}

// ParseWebhookPayload parses and validates webhook payload
func (a *FlightAwareAdapter) ParseWebhookPayload(ctx context.Context, payload []byte) (*ports.WebhookEvent, error) {
    var event struct {
        ID         string                 `json:"id"`
        Type       string                 `json:"type"`
        EventType  string                 `json:"event_type"`
        Timestamp  int64                  `json:"timestamp"`
        FlightID   string                 `json:"fa_flight_id"`
        Payload    map[string]interface{} `json:"payload"`
    }

    if err := json.Unmarshal(payload, &event); err != nil {
        return nil, fmt.Errorf("parsing webhook payload: %w", err)
    }

    return &ports.WebhookEvent{
        ID:         event.ID,
        Type:       event.Type,
        EventType:  event.EventType,
        Timestamp:  time.Unix(event.Timestamp, 0),
        FAFlightID: event.FlightID,
        Payload:    event.Payload,
    }, nil
}

C. Database Schema (PostgreSQL)

-- ============================================================================
-- TRACKING MODULE DATABASE SCHEMA
-- ============================================================================
-- 
-- This schema extends the existing SkyBrief database with tables for:
-- - Aircraft positions (24-hour TTL for performance)
-- - Flight tracks (12-month retention for compliance)
-- - Flight status events (audit trail)
-- - Tracking alerts
--
-- ============================================================================

-- Enable UUID extension if not already enabled
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Enable PostGIS for spatial queries (if not already enabled)
CREATE EXTENSION IF NOT EXISTS postgis;

-- ============================================================================
-- TABLE: tracked_flights
-- ============================================================================
-- Maps internal flights to FlightAware tracking

CREATE TABLE tracked_flights (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relationships
    flight_id UUID NOT NULL REFERENCES flights(id) ON DELETE CASCADE,
    aircraft_id UUID NOT NULL REFERENCES aircraft(id) ON DELETE CASCADE,
    operator_id UUID NOT NULL REFERENCES operators(id) ON DELETE CASCADE,

    -- Flight identifiers
    fa_flight_id VARCHAR(50) NOT NULL, -- FlightAware flight ID
    callsign VARCHAR(20),
    registration VARCHAR(20) NOT NULL,

    -- Route
    origin VARCHAR(4) NOT NULL,      -- ICAO code
    destination VARCHAR(4) NOT NULL, -- ICAO code

    -- Timing
    scheduled_out TIMESTAMP WITH TIME ZONE,
    scheduled_in TIMESTAMP WITH TIME ZONE,
    actual_out TIMESTAMP WITH TIME ZONE,
    actual_in TIMESTAMP WITH TIME ZONE,
    estimated_in TIMESTAMP WITH TIME ZONE,

    -- Status
    status VARCHAR(50) NOT NULL DEFAULT 'planned',

    -- Tracking metadata
    is_tracking BOOLEAN NOT NULL DEFAULT false,
    tracking_started_at TIMESTAMP WITH TIME ZONE,
    tracking_stopped_at TIMESTAMP WITH TIME ZONE,
    last_position_at TIMESTAMP WITH TIME ZONE,
    last_polled_at TIMESTAMP WITH TIME ZONE,

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_status CHECK (status IN ('planned', 'scheduled', 'active', 'arrived', 'diverted', 'cancelled', 'delayed')),
    CONSTRAINT uq_fa_flight_id UNIQUE (fa_flight_id)
);

-- Indexes for tracked_flights
CREATE INDEX idx_tracked_flights_operator_id ON tracked_flights(operator_id);
CREATE INDEX idx_tracked_flights_aircraft_id ON tracked_flights(aircraft_id);
CREATE INDEX idx_tracked_flights_status ON tracked_flights(status);
CREATE INDEX idx_tracked_flights_is_tracking ON tracked_flights(is_tracking) WHERE is_tracking = true;
CREATE INDEX idx_tracked_flights_fa_flight_id ON tracked_flights(fa_flight_id);
CREATE INDEX idx_tracked_flights_last_polled ON tracked_flights(last_polled_at) 
    WHERE is_tracking = true;

-- Trigger to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_tracked_flights_updated_at 
    BEFORE UPDATE ON tracked_flights 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

-- ============================================================================
-- TABLE: aircraft_positions
-- ============================================================================
-- Stores real-time position data (24-hour TTL)
-- Partitioned by date for efficient cleanup

CREATE TABLE aircraft_positions (
    id UUID NOT NULL,

    -- Relationships
    flight_id UUID NOT NULL REFERENCES flights(id) ON DELETE CASCADE,
    aircraft_id UUID NOT NULL REFERENCES aircraft(id) ON DELETE CASCADE,
    operator_id UUID NOT NULL REFERENCES operators(id) ON DELETE CASCADE,
    tracked_flight_id UUID REFERENCES tracked_flights(id) ON DELETE SET NULL,

    -- Position data
    latitude DECIMAL(10, 8) NOT NULL,
    longitude DECIMAL(11, 8) NOT NULL,
    altitude INTEGER, -- feet MSL
    ground_speed INTEGER, -- knots
    track INTEGER, -- degrees true (0-360)
    vertical_speed INTEGER, -- feet per minute

    -- Source
    source VARCHAR(50) NOT NULL DEFAULT 'flightaware',
    fa_flight_id VARCHAR(50),

    -- Timing
    position_timestamp TIMESTAMP WITH TIME ZONE NOT NULL, -- When the position was recorded
    received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Partition key
    recorded_date DATE NOT NULL GENERATED ALWAYS AS (DATE(position_timestamp)) STORED,

    -- Spatial data (optional, for PostGIS queries)
    geom GEOMETRY(Point, 4326) GENERATED ALWAYS AS (
        ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)
    ) STORED,

    PRIMARY KEY (id, recorded_date)
) PARTITION BY RANGE (recorded_date);

-- Create partitions for current and next month
-- These should be created dynamically by a maintenance job
CREATE TABLE aircraft_positions_y2024m01 PARTITION OF aircraft_positions
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE aircraft_positions_y2024m02 PARTITION OF aircraft_positions
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Indexes for aircraft_positions
CREATE INDEX idx_aircraft_positions_flight_id ON aircraft_positions(flight_id);
CREATE INDEX idx_aircraft_positions_aircraft_id ON aircraft_positions(aircraft_id);
CREATE INDEX idx_aircraft_positions_operator_id ON aircraft_positions(operator_id);
CREATE INDEX idx_aircraft_positions_timestamp ON aircraft_positions(position_timestamp);
CREATE INDEX idx_aircraft_positions_fa_flight_id ON aircraft_positions(fa_flight_id);

-- Spatial index (if using PostGIS)
CREATE INDEX idx_aircraft_positions_geom ON aircraft_positions USING GIST(geom);

-- Function to auto-create partitions (run daily via cron)
CREATE OR REPLACE FUNCTION create_aircraft_positions_partition()
RETURNS void AS $$
DECLARE
    partition_date DATE;
    partition_name TEXT;
    start_date DATE;
    end_date DATE;
BEGIN
    -- Create partition for tomorrow
    partition_date := CURRENT_DATE + INTERVAL '1 day';
    partition_name := 'aircraft_positions_y' || TO_CHAR(partition_date, 'YYYY') || 'm' || TO_CHAR(partition_date, 'MM');
    start_date := DATE_TRUNC('month', partition_date);
    end_date := start_date + INTERVAL '1 month';

    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS %I PARTITION OF aircraft_positions FOR VALUES FROM (%L) TO (%L)',
        partition_name,
        start_date,
        end_date
    );
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- TABLE: flight_tracks
-- ============================================================================
-- Stores complete flight track summaries (12-month retention)

CREATE TABLE flight_tracks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relationships
    flight_id UUID NOT NULL REFERENCES flights(id) ON DELETE CASCADE,
    aircraft_id UUID NOT NULL REFERENCES aircraft(id) ON DELETE CASCADE,
    operator_id UUID NOT NULL REFERENCES operators(id) ON DELETE CASCADE,
    tracked_flight_id UUID REFERENCES tracked_flights(id) ON DELETE SET NULL,

    -- Flight identifiers
    fa_flight_id VARCHAR(50) NOT NULL,
    callsign VARCHAR(20),
    registration VARCHAR(20) NOT NULL,

    -- Route
    origin VARCHAR(4) NOT NULL,
    destination VARCHAR(4) NOT NULL,

    -- Timing
    start_time TIMESTAMP WITH TIME ZONE NOT NULL,
    end_time TIMESTAMP WITH TIME ZONE,

    -- Track metadata
    point_count INTEGER NOT NULL DEFAULT 0,
    distance_nm DECIMAL(10, 2), -- Calculated distance in nautical miles

    -- Data storage
    track_data JSONB, -- Compressed track data for download

    -- Compliance metadata
    retention_until TIMESTAMP WITH TIME ZONE NOT NULL,

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_flight_tracks_flight_id UNIQUE (flight_id)
);

-- Indexes for flight_tracks
CREATE INDEX idx_flight_tracks_operator_id ON flight_tracks(operator_id);
CREATE INDEX idx_flight_tracks_aircraft_id ON flight_tracks(aircraft_id);
CREATE INDEX idx_flight_tracks_fa_flight_id ON flight_tracks(fa_flight_id);
CREATE INDEX idx_flight_tracks_start_time ON flight_tracks(start_time);
CREATE INDEX idx_flight_tracks_retention ON flight_tracks(retention_until) 
    WHERE retention_until < NOW();

-- ============================================================================
-- TABLE: flight_status_events
-- ============================================================================
-- Audit trail of all flight status changes

CREATE TABLE flight_status_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relationships
    flight_id UUID NOT NULL REFERENCES flights(id) ON DELETE CASCADE,
    tracked_flight_id UUID REFERENCES tracked_flights(id) ON DELETE SET NULL,
    operator_id UUID NOT NULL REFERENCES operators(id) ON DELETE CASCADE,

    -- Event details
    event_type VARCHAR(50) NOT NULL,
    previous_status VARCHAR(50),
    new_status VARCHAR(50),

    -- Data
    event_data JSONB, -- Additional context

    -- Source
    source VARCHAR(50) NOT NULL DEFAULT 'manual', -- manual, flightaware_webhook, polling, etc.

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL
);

-- Indexes for flight_status_events
CREATE INDEX idx_flight_status_events_flight_id ON flight_status_events(flight_id);
CREATE INDEX idx_flight_status_events_operator_id ON flight_status_events(operator_id);
CREATE INDEX idx_flight_status_events_created_at ON flight_status_events(created_at);
CREATE INDEX idx_flight_status_events_type ON flight_status_events(event_type);

-- ============================================================================
-- TABLE: tracking_alerts
-- ============================================================================
-- Stores alerts generated from tracking events

CREATE TABLE tracking_alerts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relationships
    flight_id UUID NOT NULL REFERENCES flights(id) ON DELETE CASCADE,
    aircraft_id UUID NOT NULL REFERENCES aircraft(id) ON DELETE CASCADE,
    operator_id UUID NOT NULL REFERENCES operators(id) ON DELETE CASCADE,
    tracked_flight_id UUID REFERENCES tracked_flights(id) ON DELETE SET NULL,

    -- Alert details
    alert_type VARCHAR(50) NOT NULL,
    severity VARCHAR(20) NOT NULL DEFAULT 'medium',
    title VARCHAR(255) NOT NULL,
    message TEXT NOT NULL,

    -- Change tracking
    previous_value TEXT,
    current_value TEXT,

    -- Source
    source VARCHAR(50) NOT NULL, -- flightaware_webhook, weather_correlation, system

    -- Acknowledgment
    acknowledged_at TIMESTAMP WITH TIME ZONE,
    acknowledged_by UUID REFERENCES users(id) ON DELETE SET NULL,

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_alert_type CHECK (alert_type IN (
        'diversion', 'arrival', 'departure', 'cancellation', 
        'delay', 'weather_enroute', 'eta_change', 'other'
    )),
    CONSTRAINT chk_severity CHECK (severity IN ('low', 'medium', 'high', 'critical'))
);

-- Indexes for tracking_alerts
CREATE INDEX idx_tracking_alerts_operator_id ON tracking_alerts(operator_id);
CREATE INDEX idx_tracking_alerts_flight_id ON tracking_alerts(flight_id);
CREATE INDEX idx_tracking_alerts_aircraft_id ON tracking_alerts(aircraft_id);
CREATE INDEX idx_tracking_alerts_type ON tracking_alerts(alert_type);
CREATE INDEX idx_tracking_alerts_severity ON tracking_alerts(severity);
CREATE INDEX idx_tracking_alerts_created_at ON tracking_alerts(created_at);
CREATE INDEX idx_tracking_alerts_unacknowledged ON tracking_alerts(operator_id, created_at) 
    WHERE acknowledged_at IS NULL;

-- ============================================================================
-- TABLE: weather_correlation_checks
-- ============================================================================
-- Records when weather was checked against flight positions

CREATE TABLE weather_correlation_checks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relationships
    flight_id UUID NOT NULL REFERENCES flights(id) ON DELETE CASCADE,
    tracked_flight_id UUID REFERENCES tracked_flights(id) ON DELETE SET NULL,
    operator_id UUID NOT NULL REFERENCES operators(id) ON DELETE CASCADE,

    -- Position data
    position_id UUID NOT NULL,
    latitude DECIMAL(10, 8) NOT NULL,
    longitude DECIMAL(11, 8) NOT NULL,
    altitude INTEGER,

    -- Weather data at position
    metar_station VARCHAR(4),
    metar_data TEXT,
    sigmet_affected BOOLEAN DEFAULT false,
    sigmet_data JSONB,

    -- Correlation results
    alerts_generated INTEGER DEFAULT 0,
    alert_ids UUID[],

    -- Metadata
    checked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

-- Indexes for weather_correlation_checks
CREATE INDEX idx_weather_correlation_flight_id ON weather_correlation_checks(flight_id);
CREATE INDEX idx_weather_correlation_operator_id ON weather_correlation_checks(operator_id);
CREATE INDEX idx_weather_correlation_checked_at ON weather_correlation_checks(checked_at);

-- ============================================================================
-- CLEANUP FUNCTIONS (TTL Enforcement)
-- ============================================================================

-- Function to clean up old positions (24-hour TTL)
CREATE OR REPLACE FUNCTION cleanup_old_positions()
RETURNS INTEGER AS $$
DECLARE
    deleted_count INTEGER;
BEGIN
    DELETE FROM aircraft_positions 
    WHERE position_timestamp < NOW() - INTERVAL '24 hours';

    GET DIAGNOSTICS deleted_count = ROW_COUNT;
    RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;

-- Function to clean up old correlation checks (7-day TTL)
CREATE OR REPLACE FUNCTION cleanup_old_correlation_checks()
RETURNS INTEGER AS $$
DECLARE
    deleted_count INTEGER;
BEGIN
    DELETE FROM weather_correlation_checks 
    WHERE checked_at < NOW() - INTERVAL '7 days';

    GET DIAGNOSTICS deleted_count = ROW_COUNT;
    RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;

-- Function to archive old tracks after 12 months (soft delete / compress)
CREATE OR REPLACE FUNCTION archive_old_tracks()
RETURNS INTEGER AS $$
DECLARE
    archived_count INTEGER;
BEGIN
    -- Compress track data and mark for cold storage
    UPDATE flight_tracks 
    SET track_data = NULL -- Data moved to cold storage
    WHERE retention_until < NOW() - INTERVAL '12 months'
    AND track_data IS NOT NULL;

    GET DIAGNOSTICS archived_count = ROW_COUNT;
    RETURN archived_count;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- VIEWS
-- ============================================================================

-- Active flights view (for quick lookup)
CREATE OR REPLACE VIEW active_tracked_flights AS
SELECT 
    tf.*,
    a.type as aircraft_type,
    a.icao_type,
    ap.latitude as current_latitude,
    ap.longitude as current_longitude,
    ap.altitude as current_altitude,
    ap.ground_speed as current_ground_speed
FROM tracked_flights tf
JOIN aircraft a ON tf.aircraft_id = a.id
LEFT JOIN LATERAL (
    SELECT * FROM aircraft_positions 
    WHERE aircraft_id = tf.aircraft_id 
    ORDER BY position_timestamp DESC 
    LIMIT 1
) ap ON true
WHERE tf.is_tracking = true 
AND tf.status IN ('active', 'scheduled');

-- Fleet status view (for dashboard)
CREATE OR REPLACE VIEW fleet_status AS
SELECT 
    operator_id,
    COUNT(*) FILTER (WHERE status = 'active') as active_flights,
    COUNT(*) FILTER (WHERE status = 'scheduled') as scheduled_flights,
    COUNT(*) FILTER (WHERE status = 'arrived' AND actual_in > NOW() - INTERVAL '24 hours') as recent_arrivals,
    COUNT(*) FILTER (WHERE status = 'diverted') as diverted_flights,
    COUNT(*) FILTER (WHERE is_tracking = true) as tracked_count,
    MAX(last_position_at) as last_update
FROM tracked_flights
GROUP BY operator_id;

-- Unacknowledged alerts view
CREATE OR REPLACE VIEW unacknowledged_alerts AS
SELECT 
    ta.*,
    f.callsign,
    f.origin,
    f.destination,
    a.registration,
    a.type as aircraft_type
FROM tracking_alerts ta
JOIN flights f ON ta.flight_id = f.id
JOIN aircraft a ON ta.aircraft_id = a.id
WHERE ta.acknowledged_at IS NULL
ORDER BY ta.created_at DESC;

D. Webhook Handler (internal/adapters/http/handlers/webhooks.go)

package handlers

import (
    "context"
    "encoding/json"
    "io"
    "net/http"
    "time"

    "github.com/labstack/echo/v4"
    "github.com/google/uuid"

    "github.com/skybrief/internal/ports"
    "github.com/skybrief/internal/application/tracking"
    "github.com/skybrief/pkg/websocket"
    "github.com/skybrief/pkg/logger"
)

// WebhookHandler handles incoming webhooks from external services
type WebhookHandler struct {
    trackingService  ports.FlightTrackingService
    trackingProvider ports.ExternalTrackingProvider
    alertService     *tracking.AlertService
    wsHub            *websocket.Hub
    logger           logger.Logger
}

// NewWebhookHandler creates a new webhook handler
func NewWebhookHandler(
    trackingService ports.FlightTrackingService,
    trackingProvider ports.ExternalTrackingProvider,
    alertService *tracking.AlertService,
    wsHub *websocket.Hub,
    logger logger.Logger,
) *WebhookHandler {
    return &WebhookHandler{
        trackingService:  trackingService,
        trackingProvider: trackingProvider,
        alertService:     alertService,
        wsHub:            wsHub,
        logger:           logger,
    }
}

// RegisterRoutes registers webhook routes
func (h *WebhookHandler) RegisterRoutes(e *echo.Echo) {
    webhooks := e.Group("/api/v1/webhooks")

    // FlightAware webhooks
    webhooks.POST("/flightaware", h.HandleFlightAwareWebhook)
}

// HandleFlightAwareWebhook handles incoming webhooks from FlightAware
// POST /api/v1/webhooks/flightaware
func (h *WebhookHandler) HandleFlightAwareWebhook(c echo.Context) error {
    ctx := c.Request().Context()

    // Read payload
    payload, err := io.ReadAll(c.Request().Body)
    if err != nil {
        h.logger.Error("Failed to read webhook payload", "error", err)
        return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body")
    }
    defer c.Request().Body.Close()

    // Verify signature
    signature := c.Request().Header.Get("X-FlightAware-Signature")
    if signature == "" {
        h.logger.Error("Missing webhook signature")
        return echo.NewHTTPError(http.StatusUnauthorized, "Missing signature")
    }

    if err := h.trackingProvider.VerifyWebhookSignature(ctx, payload, signature); err != nil {
        h.logger.Error("Invalid webhook signature", "error", err)
        return echo.NewHTTPError(http.StatusUnauthorized, "Invalid signature")
    }

    // Parse webhook event
    event, err := h.trackingProvider.ParseWebhookPayload(ctx, payload)
    if err != nil {
        h.logger.Error("Failed to parse webhook payload", "error", err)
        return echo.NewHTTPError(http.StatusBadRequest, "Invalid payload")
    }

    h.logger.Info("Received FlightAware webhook",
        "event_type", event.EventType,
        "flight_id", event.FAFlightID,
    )

    // Process based on event type
    switch event.EventType {
    case "departure":
        err = h.handleDeparture(ctx, event)
    case "arrival":
        err = h.handleArrival(ctx, event)
    case "diversion":
        err = h.handleDiversion(ctx, event)
    case "cancellation":
        err = h.handleCancellation(ctx, event)
    case "delay":
        err = h.handleDelay(ctx, event)
    case "position":
        err = h.handlePositionUpdate(ctx, event)
    case "eta_change":
        err = h.handleETAChange(ctx, event)
    default:
        h.logger.Info("Unhandled webhook event type", "type", event.EventType)
    }

    if err != nil {
        h.logger.Error("Failed to process webhook event",
            "event_type", event.EventType,
            "error", err,
        )
        return echo.NewHTTPError(http.StatusInternalServerError, "Processing failed")
    }

    return c.NoContent(http.StatusOK)
}

// handleDeparture processes departure events
func (h *WebhookHandler) handleDeparture(ctx context.Context, event *ports.WebhookEvent) error {
    // Get tracked flight by FA flight ID
    trackedFlight, err := h.getTrackedFlightByFAID(ctx, event.FAFlightID)
    if err != nil {
        return err
    }

    // Update flight status
    if err := h.trackingService.StopTracking(ctx, trackedFlight.FlightID); err != nil {
        h.logger.Error("Failed to stop tracking on departure", "error", err)
    }

    // Create alert
    alert := &ports.TrackingAlert{
        FlightID:     trackedFlight.FlightID,
        AircraftID:   trackedFlight.AircraftID,
        OperatorID:   trackedFlight.OperatorID,
        Type:         ports.AlertTypeDeparture,
        Severity:     "low",
        Title:        "Flight Departed",
        Message:      fmt.Sprintf("Flight %s has departed from %s", trackedFlight.Callsign, trackedFlight.Origin),
        Source:       "flightaware_webhook",
        CreatedAt:    time.Now(),
    }

    if err := h.trackingService.SetAlert(ctx, alert); err != nil {
        h.logger.Error("Failed to create departure alert", "error", err)
    }

    // Broadcast via WebSocket
    h.broadcastUpdate(trackedFlight.OperatorID, map[string]interface{}{
        "type":       "departure",
        "flight_id":  trackedFlight.FlightID,
        "callsign":   trackedFlight.Callsign,
        "timestamp":  event.Timestamp,
    })

    return nil
}

// handleArrival processes arrival events
func (h *WebhookHandler) handleArrival(ctx context.Context, event *ports.WebhookEvent) error {
    trackedFlight, err := h.getTrackedFlightByFAID(ctx, event.FAFlightID)
    if err != nil {
        return err
    }

    // Stop tracking
    if err := h.trackingService.StopTracking(ctx, trackedFlight.FlightID); err != nil {
        h.logger.Error("Failed to stop tracking on arrival", "error", err)
    }

    // Get actual arrival time from payload
    var actualIn time.Time
    if actualInStr, ok := event.Payload["actual_in"].(string); ok {
        actualIn, _ = time.Parse(time.RFC3339, actualInStr)
    }

    // Create alert
    alert := &ports.TrackingAlert{
        FlightID:     trackedFlight.FlightID,
        AircraftID:   trackedFlight.AircraftID,
        OperatorID:   trackedFlight.OperatorID,
        Type:         ports.AlertTypeArrival,
        Severity:     "low",
        Title:        "Flight Arrived",
        Message:      fmt.Sprintf("Flight %s has arrived at %s", trackedFlight.Callsign, trackedFlight.Destination),
        Source:       "flightaware_webhook",
        CreatedAt:    time.Now(),
    }

    if err := h.trackingService.SetAlert(ctx, alert); err != nil {
        h.logger.Error("Failed to create arrival alert", "error", err)
    }

    // Broadcast via WebSocket
    h.broadcastUpdate(trackedFlight.OperatorID, map[string]interface{}{
        "type":       "arrival",
        "flight_id":  trackedFlight.FlightID,
        "callsign":   trackedFlight.Callsign,
        "destination": trackedFlight.Destination,
        "timestamp":  event.Timestamp,
    })

    return nil
}

// handleDiversion processes diversion events
func (h *WebhookHandler) handleDiversion(ctx context.Context, event *ports.WebhookEvent) error {
    trackedFlight, err := h.getTrackedFlightByFAID(ctx, event.FAFlightID)
    if err != nil {
        return err
    }

    // Get diversion details
    var diversionAirport string
    if airport, ok := event.Payload["diversion_airport"].(string); ok {
        diversionAirport = airport
    }

    // Create critical alert
    alert := &ports.TrackingAlert{
        FlightID:      trackedFlight.FlightID,
        AircraftID:    trackedFlight.AircraftID,
        OperatorID:    trackedFlight.OperatorID,
        Type:          ports.AlertTypeDiversion,
        Severity:      "critical",
        Title:         "FLIGHT DIVERTED",
        Message:       fmt.Sprintf("Flight %s has diverted to %s", trackedFlight.Callsign, diversionAirport),
        CurrentValue:  &diversionAirport,
        Source:        "flightaware_webhook",
        CreatedAt:     time.Now(),
    }

    if err := h.trackingService.SetAlert(ctx, alert); err != nil {
        h.logger.Error("Failed to create diversion alert", "error", err)
    }

    // Trigger immediate weather check for diversion airport
    h.alertService.CheckWeatherForAirport(ctx, trackedFlight.OperatorID, diversionAirport)

    // Broadcast via WebSocket
    h.broadcastUpdate(trackedFlight.OperatorID, map[string]interface{}{
        "type":              "diversion",
        "flight_id":         trackedFlight.FlightID,
        "callsign":          trackedFlight.Callsign,
        "diversion_airport": diversionAirport,
        "severity":          "critical",
    })

    return nil
}

// handleCancellation processes cancellation events
func (h *WebhookHandler) handleCancellation(ctx context.Context, event *ports.WebhookEvent) error {
    trackedFlight, err := h.getTrackedFlightByFAID(ctx, event.FAFlightID)
    if err != nil {
        return err
    }

    // Stop tracking
    if err := h.trackingService.StopTracking(ctx, trackedFlight.FlightID); err != nil {
        h.logger.Error("Failed to stop tracking on cancellation", "error", err)
    }

    // Create alert
    alert := &ports.TrackingAlert{
        FlightID:     trackedFlight.FlightID,
        AircraftID:   trackedFlight.AircraftID,
        OperatorID:   trackedFlight.OperatorID,
        Type:         ports.AlertTypeCancellation,
        Severity:     "high",
        Title:        "Flight Cancelled",
        Message:      fmt.Sprintf("Flight %s has been cancelled", trackedFlight.Callsign),
        Source:       "flightaware_webhook",
        CreatedAt:    time.Now(),
    }

    if err := h.trackingService.SetAlert(ctx, alert); err != nil {
        h.logger.Error("Failed to create cancellation alert", "error", err)
    }

    // Broadcast via WebSocket
    h.broadcastUpdate(trackedFlight.OperatorID, map[string]interface{}{
        "type":       "cancellation",
        "flight_id":  trackedFlight.FlightID,
        "callsign":   trackedFlight.Callsign,
    })

    return nil
}

// handleDelay processes delay events
func (h *WebhookHandler) handleDelay(ctx context.Context, event *ports.WebhookEvent) error {
    trackedFlight, err := h.getTrackedFlightByFAID(ctx, event.FAFlightID)
    if err != nil {
        return err
    }

    // Get delay details
    var delayMinutes int
    if delay, ok := event.Payload["delay_minutes"].(float64); ok {
        delayMinutes = int(delay)
    }

    delayStr := fmt.Sprintf("%d minutes", delayMinutes)

    // Create alert
    severity := "medium"
    if delayMinutes > 60 {
        severity = "high"
    }

    alert := &ports.TrackingAlert{
        FlightID:      trackedFlight.FlightID,
        AircraftID:    trackedFlight.AircraftID,
        OperatorID:    trackedFlight.OperatorID,
        Type:          ports.AlertTypeDelay,
        Severity:      severity,
        Title:         "Flight Delayed",
        Message:       fmt.Sprintf("Flight %s delayed by %s", trackedFlight.Callsign, delayStr),
        CurrentValue:  &delayStr,
        Source:        "flightaware_webhook",
        CreatedAt:     time.Now(),
    }

    if err := h.trackingService.SetAlert(ctx, alert); err != nil {
        h.logger.Error("Failed to create delay alert", "error", err)
    }

    // Broadcast via WebSocket
    h.broadcastUpdate(trackedFlight.OperatorID, map[string]interface{}{
        "type":          "delay",
        "flight_id":     trackedFlight.FlightID,
        "callsign":      trackedFlight.Callsign,
        "delay_minutes": delayMinutes,
    })

    return nil
}

// handleETAChange processes ETA update events
func (h *WebhookHandler) handleETAChange(ctx context.Context, event *ports.WebhookEvent) error {
    trackedFlight, err := h.getTrackedFlightByFAID(ctx, event.FAFlightID)
    if err != nil {
        return err
    }

    // Get ETA details
    var newETA time.Time
    var previousETA time.Time

    if eta, ok := event.Payload["estimated_in"].(string); ok {
        newETA, _ = time.Parse(time.RFC3339, eta)
    }
    if prevETA, ok := event.Payload["previous_eta"].(string); ok {
        previousETA, _ = time.Parse(time.RFC3339, prevETA)
    }

    diff := newETA.Sub(previousETA)
    diffMinutes := int(diff.Minutes())

    // Only alert if significant change (> 10 minutes)
    if abs(diffMinutes) < 10 {
        return nil
    }

    changeStr := fmt.Sprintf("%d minutes", diffMinutes)
    if diffMinutes > 0 {
        changeStr = fmt.Sprintf("+%s", changeStr)
    }

    alert := &ports.TrackingAlert{
        FlightID:      trackedFlight.FlightID,
        AircraftID:    trackedFlight.AircraftID,
        OperatorID:    trackedFlight.OperatorID,
        Type:          ports.AlertTypeETAChange,
        Severity:      "medium",
        Title:         "ETA Updated",
        Message:       fmt.Sprintf("Flight %s ETA changed by %s", trackedFlight.Callsign, changeStr),
        PreviousValue: strPtr(previousETA.Format("15:04")),
        CurrentValue:  strPtr(newETA.Format("15:04")),
        Source:        "flightaware_webhook",
        CreatedAt:     time.Now(),
    }

    if err := h.trackingService.SetAlert(ctx, alert); err != nil {
        h.logger.Error("Failed to create ETA change alert", "error", err)
    }

    // Broadcast via WebSocket
    h.broadcastUpdate(trackedFlight.OperatorID, map[string]interface{}{
        "type":       "eta_change",
        "flight_id":  trackedFlight.FlightID,
        "callsign":   trackedFlight.Callsign,
        "new_eta":    newETA,
        "delta_minutes": diffMinutes,
    })

    return nil
}

// handlePositionUpdate processes position update events
func (h *WebhookHandler) handlePositionUpdate(ctx context.Context, event *ports.WebhookEvent) error {
    // Position updates are handled by the polling service
    // This webhook handler can trigger immediate updates if needed

    trackedFlight, err := h.getTrackedFlightByFAID(ctx, event.FAFlightID)
    if err != nil {
        return err
    }

    // Broadcast via WebSocket for real-time map updates
    h.broadcastUpdate(trackedFlight.OperatorID, map[string]interface{}{
        "type":       "position_update",
        "flight_id":  trackedFlight.FlightID,
        "callsign":   trackedFlight.Callsign,
        "position":   event.Payload,
    })

    return nil
}

// getTrackedFlightByFAID retrieves a tracked flight by FlightAware flight ID
func (h *WebhookHandler) getTrackedFlightByFAID(ctx context.Context, faFlightID string) (*ports.TrackedFlight, error) {
    // This would query the database for the tracked flight
    // Implementation depends on your repository layer

    // Placeholder implementation
    return nil, fmt.Errorf("not implemented")
}

// broadcastUpdate sends update to all connected clients for an operator
func (h *WebhookHandler) broadcastUpdate(operatorID uuid.UUID, data map[string]interface{}) {
    if h.wsHub == nil {
        return
    }

    message := websocket.Message{
        Type:       "tracking_update",
        OperatorID: operatorID,
        Data:       data,
        Timestamp:  time.Now(),
    }

    h.wsHub.BroadcastToOperator(operatorID, message)
}

// Helper function
func strPtr(s string) *string {
    return &s
}

func abs(n int) int {
    if n < 0 {
        return -n
    }
    return n
}

E. Polling Service (internal/application/tracking/poller.go)

package tracking

import (
    "context"
    "fmt"
    "sync"
    "time"

    "github.com/google/uuid"

    "github.com/skybrief/internal/ports"
    "github.com/skybrief/internal/domain/weather"
    "github.com/skybrief/pkg/logger"
    "github.com/skybrief/pkg/websocket"
)

// PollerConfig holds configuration for the tracking poller
type PollerConfig struct {
    PollingInterval       time.Duration // Default: 60 seconds
    WeatherCheckInterval  time.Duration // Default: 5 minutes
    MaxConcurrentPolls    int           // Default: 10
    EnableWeatherCorrelation bool
}

// DefaultPollerConfig returns default configuration
func DefaultPollerConfig() PollerConfig {
    return PollerConfig{
        PollingInterval:          60 * time.Second,
        WeatherCheckInterval:     5 * time.Minute,
        MaxConcurrentPolls:       10,
        EnableWeatherCorrelation: true,
    }
}

// PollingService manages background polling of active flights
type PollingService struct {
    config               PollerConfig
    trackingProvider     ports.ExternalTrackingProvider
    positionRepo         ports.AircraftPositionRepository
    trackingService      ports.FlightTrackingService
    trackedFlightRepo    TrackedFlightRepository
    weatherService       weather.Service
    wsHub                *websocket.Hub
    cache                Cache // Redis
    logger               logger.Logger

    // Control
    ctx                  context.Context
    cancel               context.CancelFunc
    wg                   sync.WaitGroup
    mu                   sync.RWMutex
    activeFlights        map[uuid.UUID]*trackedFlightState
}

// trackedFlightState tracks the state of an actively polled flight
type trackedFlightState struct {
    FlightID          uuid.UUID
    FAFlightID        string
    LastPolled        time.Time
    LastPosition      *ports.AircraftPosition
    LastWeatherCheck  time.Time
    ConsecutiveErrors int
    IsPolling         bool
}

// NewPollingService creates a new polling service
func NewPollingService(
    config PollerConfig,
    trackingProvider ports.ExternalTrackingProvider,
    positionRepo ports.AircraftPositionRepository,
    trackingService ports.FlightTrackingService,
    trackedFlightRepo TrackedFlightRepository,
    weatherService weather.Service,
    wsHub *websocket.Hub,
    cache Cache,
    logger logger.Logger,
) *PollingService {
    ctx, cancel := context.WithCancel(context.Background())

    return &PollingService{
        config:            config,
        trackingProvider:  trackingProvider,
        positionRepo:      positionRepo,
        trackingService:   trackingService,
        trackedFlightRepo: trackedFlightRepo,
        weatherService:    weatherService,
        wsHub:             wsHub,
        cache:             cache,
        logger:            logger,
        ctx:               ctx,
        cancel:            cancel,
        activeFlights:     make(map[uuid.UUID]*trackedFlightState),
    }
}

// Start begins the polling service
func (s *PollingService) Start() error {
    s.logger.Info("Starting tracking polling service",
        "polling_interval", s.config.PollingInterval,
        "weather_interval", s.config.WeatherCheckInterval,
    )

    // Start the main polling loop
    s.wg.Add(1)
    go s.pollingLoop()

    // Start the weather correlation loop
    if s.config.EnableWeatherCorrelation {
        s.wg.Add(1)
        go s.weatherCorrelationLoop()
    }

    return nil
}

// Stop gracefully stops the polling service
func (s *PollingService) Stop() error {
    s.logger.Info("Stopping tracking polling service")

    s.cancel()
    s.wg.Wait()

    return nil
}

// AddFlight adds a flight to the polling list
func (s *PollingService) AddFlight(flightID uuid.UUID, faFlightID string) {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.activeFlights[flightID] = &trackedFlightState{
        FlightID:   flightID,
        FAFlightID: faFlightID,
        LastPolled: time.Time{},
        IsPolling:  true,
    }

    s.logger.Info("Added flight to polling", "flight_id", flightID, "fa_flight_id", faFlightID)
}

// RemoveFlight removes a flight from the polling list
func (s *PollingService) RemoveFlight(flightID uuid.UUID) {
    s.mu.Lock()
    defer s.mu.Unlock()

    delete(s.activeFlights, flightID)
    s.logger.Info("Removed flight from polling", "flight_id", flightID)
}

// pollingLoop runs the main polling loop
func (s *PollingService) pollingLoop() {
    defer s.wg.Done()

    ticker := time.NewTicker(s.config.PollingInterval)
    defer ticker.Stop()

    // Semaphore for concurrent polls
    semaphore := make(chan struct{}, s.config.MaxConcurrentPolls)

    for {
        select {
        case <-s.ctx.Done():
            return
        case <-ticker.C:
            s.pollAllFlights(semaphore)
        }
    }
}

// pollAllFlights polls all active flights
func (s *PollingService) pollAllFlights(semaphore chan struct{}) {
    s.mu.RLock()
    flights := make([]*trackedFlightState, 0, len(s.activeFlights))
    for _, state := range s.activeFlights {
        if state.IsPolling {
            flights = append(flights, state)
        }
    }
    s.mu.RUnlock()

    for _, flight := range flights {
        select {
        case semaphore <- struct{}{}:
            s.wg.Add(1)
            go func(f *trackedFlightState) {
                defer s.wg.Done()
                defer func() { <-semaphore }()

                if err := s.pollFlight(f); err != nil {
                    s.logger.Error("Failed to poll flight",
                        "flight_id", f.FlightID,
                        "fa_flight_id", f.FAFlightID,
                        "error", err,
                    )

                    // Track consecutive errors
                    s.mu.Lock()
                    f.ConsecutiveErrors++
                    if f.ConsecutiveErrors > 5 {
                        s.logger.Warn("Disabling polling due to consecutive errors",
                            "flight_id", f.FlightID,
                            "errors", f.ConsecutiveErrors,
                        )
                        f.IsPolling = false
                    }
                    s.mu.Unlock()
                } else {
                    s.mu.Lock()
                    f.ConsecutiveErrors = 0
                    s.mu.Unlock()
                }
            }(flight)
        default:
            // Semaphore full, skip this cycle
            s.logger.Warn("Max concurrent polls reached, skipping flight", "flight_id", flight.FlightID)
        }
    }
}

// pollFlight polls a single flight and stores the position
func (s *PollingService) pollFlight(state *trackedFlightState) error {
    ctx, cancel := context.WithTimeout(s.ctx, 30*time.Second)
    defer cancel()

    // Fetch current position from FlightAware
    position, err := s.trackingProvider.GetFlightPosition(ctx, state.FAFlightID)
    if err != nil {
        return fmt.Errorf("fetching position: %w", err)
    }

    // Update last polled time
    s.mu.Lock()
    state.LastPolled = time.Now()
    s.mu.Unlock()

    // Skip if position hasn't changed (same timestamp)
    if state.LastPosition != nil && position.Timestamp.Equal(state.LastPosition.Timestamp) {
        return nil
    }

    // Get tracked flight details for IDs
    trackedFlight, err := s.trackedFlightRepo.GetByFAFlightID(ctx, state.FAFlightID)
    if err != nil {
        return fmt.Errorf("getting tracked flight: %w", err)
    }

    // Convert to domain position
    domainPosition := &ports.AircraftPosition{
        ID:            uuid.New(),
        FlightID:      trackedFlight.FlightID,
        AircraftID:    trackedFlight.AircraftID,
        OperatorID:    trackedFlight.OperatorID,
        Latitude:      position.Latitude,
        Longitude:     position.Longitude,
        Altitude:      position.Altitude,
        GroundSpeed:   position.GroundSpeed,
        Track:         position.Track,
        VerticalSpeed: position.VerticalSpeed,
        Source:        "flightaware",
        Timestamp:     position.Timestamp,
        ReceivedAt:    time.Now(),
        FAFlightID:    state.FAFlightID,
    }

    // Save position to database
    if err := s.positionRepo.SavePosition(ctx, domainPosition); err != nil {
        return fmt.Errorf("saving position: %w", err)
    }

    // Update state
    s.mu.Lock()
    state.LastPosition = domainPosition
    s.mu.Unlock()

    // Cache for quick access
    cacheKey := fmt.Sprintf("position:%s", trackedFlight.AircraftID)
    s.cache.Set(ctx, cacheKey, domainPosition, 5*time.Minute)

    // Broadcast via WebSocket
    if s.wsHub != nil {
        s.wsHub.BroadcastToOperator(trackedFlight.OperatorID, websocket.Message{
            Type:       "position",
            OperatorID: trackedFlight.OperatorID,
            Data:       domainPosition,
            Timestamp:  time.Now(),
        })
    }

    s.logger.Debug("Polled flight position",
        "flight_id", state.FlightID,
        "fa_flight_id", state.FAFlightID,
        "altitude", position.Altitude,
        "speed", position.GroundSpeed,
    )

    return nil
}

// weatherCorrelationLoop periodically checks weather for active flights
func (s *PollingService) weatherCorrelationLoop() {
    defer s.wg.Done()

    ticker := time.NewTicker(s.config.WeatherCheckInterval)
    defer ticker.Stop()

    for {
        select {
        case <-s.ctx.Done():
            return
        case <-ticker.C:
            s.checkWeatherForActiveFlights()
        }
    }
}

// checkWeatherForActiveFlights checks weather for all active flights
func (s *PollingService) checkWeatherForActiveFlights() {
    s.mu.RLock()
    flights := make([]*trackedFlightState, 0, len(s.activeFlights))
    for _, state := range s.activeFlights {
        if state.IsPolling && state.LastPosition != nil {
            // Check if enough time has passed since last weather check
            if time.Since(state.LastWeatherCheck) >= s.config.WeatherCheckInterval {
                flights = append(flights, state)
            }
        }
    }
    s.mu.RUnlock()

    for _, flight := range flights {
        s.wg.Add(1)
        go func(f *trackedFlightState) {
            defer s.wg.Done()

            if err := s.correlateWeatherForFlight(f); err != nil {
                s.logger.Error("Failed to correlate weather",
                    "flight_id", f.FlightID,
                    "error", err,
                )
            } else {
                s.mu.Lock()
                f.LastWeatherCheck = time.Now()
                s.mu.Unlock()
            }
        }(flight)
    }
}

// correlateWeatherForFlight checks weather at the aircraft's current position
func (s *PollingService) correlateWeatherForFlight(state *trackedFlightState) error {
    ctx, cancel := context.WithTimeout(s.ctx, 30*time.Second)
    defer cancel()

    if state.LastPosition == nil {
        return nil
    }

    // Get tracked flight details
    trackedFlight, err := s.trackedFlightRepo.GetByFAFlightID(ctx, state.FAFlightID)
    if err != nil {
        return fmt.Errorf("getting tracked flight: %w", err)
    }

    // Find nearest airport with METAR
    nearestAirport, err := s.findNearestAirport(ctx, state.LastPosition.Latitude, state.LastPosition.Longitude)
    if err != nil {
        return fmt.Errorf("finding nearest airport: %w", err)
    }

    // Fetch weather for nearest airport
    metar, err := s.weatherService.GetMETAR(ctx, nearestAirport)
    if err != nil {
        return fmt.Errorf("fetching METAR: %w", err)
    }

    // Check for SIGMETs along the route
    sigmets, err := s.weatherService.GetSIGMETsForArea(ctx, 
        state.LastPosition.Latitude, 
        state.LastPosition.Longitude,
        50, // 50nm radius
    )
    if err != nil {
        s.logger.Error("Failed to fetch SIGMETs", "error", err)
    }

    // Analyze weather conditions
    alerts := s.analyzeWeatherConditions(trackedFlight, state.LastPosition, metar, sigmets)

    // Generate alerts if needed
    for _, alert := range alerts {
        if err := s.trackingService.SetAlert(ctx, &alert); err != nil {
            s.logger.Error("Failed to create weather alert", "error", err)
        }
    }

    // Record correlation check
    correlation := &WeatherCorrelationCheck{
        FlightID:       trackedFlight.FlightID,
        TrackedFlightID: &trackedFlight.ID,
        OperatorID:     trackedFlight.OperatorID,
        PositionID:     state.LastPosition.ID,
        Latitude:       state.LastPosition.Latitude,
        Longitude:      state.LastPosition.Longitude,
        Altitude:       state.LastPosition.Altitude,
        METARStation:   nearestAirport,
        METARData:      metar.Raw,
        SIGMETAffected: len(sigmets) > 0,
        SIGMETData:     sigmets,
        AlertsGenerated: len(alerts),
        CheckedAt:      time.Now(),
    }

    if err := s.saveCorrelationCheck(ctx, correlation); err != nil {
        s.logger.Error("Failed to save correlation check", "error", err)
    }

    return nil
}

// findNearestAirport finds the nearest airport to a given position
func (s *PollingService) findNearestAirport(ctx context.Context, lat, lon float64) (string, error) {
    // This would query your airports database for the nearest airport
    // Implementation depends on your airport repository

    // Placeholder - return a hardcoded value or implement spatial query
    return "", fmt.Errorf("not implemented")
}

// analyzeWeatherConditions analyzes weather and generates alerts
func (s *PollingService) analyzeWeatherConditions(
    trackedFlight *ports.TrackedFlight,
    position *ports.AircraftPosition,
    metar *weather.METAR,
    sigmets []weather.SIGMET,
) []ports.TrackingAlert {
    var alerts []ports.TrackingAlert

    // Check for severe weather in SIGMETs
    for _, sigmet := range sigmets {
        alert := ports.TrackingAlert{
            FlightID:   trackedFlight.FlightID,
            AircraftID: trackedFlight.AircraftID,
            OperatorID: trackedFlight.OperatorID,
            Type:       ports.AlertTypeWeatherEnRoute,
            Severity:   "high",
            Title:      "SIGMET Active Along Route",
            Message:    fmt.Sprintf("SIGMET for %s: %s", sigmet.Phenomenon, sigmet.Description),
            Source:     "weather_correlation",
            CreatedAt:  time.Now(),
        }
        alerts = append(alerts, alert)
    }

    // Check for deteriorating conditions
    if metar != nil && metar.Visibility < 5000 { // Less than 5km visibility
        alert := ports.TrackingAlert{
            FlightID:   trackedFlight.FlightID,
            AircraftID: trackedFlight.AircraftID,
            OperatorID: trackedFlight.OperatorID,
            Type:       ports.AlertTypeWeatherEnRoute,
            Severity:   "medium",
            Title:      "Low Visibility Along Route",
            Message:    fmt.Sprintf("Visibility at %s: %dm", metar.Station, metar.Visibility),
            Source:     "weather_correlation",
            CreatedAt:  time.Now(),
        }
        alerts = append(alerts, alert)
    }

    return alerts
}

// GetActiveFlights returns the list of currently tracked flights
func (s *PollingService) GetActiveFlights() []uuid.UUID {
    s.mu.RLock()
    defer s.mu.RUnlock()

    flights := make([]uuid.UUID, 0, len(s.activeFlights))
    for id := range s.activeFlights {
        flights = append(flights, id)
    }
    return flights
}

// TrackedFlightRepository defines the interface for tracked flight storage
type TrackedFlightRepository interface {
    GetByFAFlightID(ctx context.Context, faFlightID string) (*ports.TrackedFlight, error)
    GetByFlightID(ctx context.Context, flightID uuid.UUID) (*ports.TrackedFlight, error)
    ListActive(ctx context.Context) ([]*ports.TrackedFlight, error)
}

// WeatherCorrelationCheck represents a weather correlation record
type WeatherCorrelationCheck struct {
    ID              uuid.UUID
    FlightID        uuid.UUID
    TrackedFlightID *uuid.UUID
    OperatorID      uuid.UUID
    PositionID      uuid.UUID
    Latitude        float64
    Longitude       float64
    Altitude        int
    METARStation    string
    METARData       string
    SIGMETAffected  bool
    SIGMETData      []weather.SIGMET
    AlertsGenerated int
    CheckedAt       time.Time
}

// saveCorrelationCheck saves the correlation check to database
func (s *PollingService) saveCorrelationCheck(ctx context.Context, check *WeatherCorrelationCheck) error {
    // Implementation depends on your repository layer
    return nil
}

4. Frontend Implementation

A. Fleet Dashboard Map Component (React + Mapbox GL JS)

// frontend/src/components/fleet/FleetDashboard.tsx
import React, { useEffect, useRef, useState, useCallback } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useAuth } from '@clerk/clerk-react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { AircraftMarker } from './AircraftMarker';
import { WeatherOverlay } from './WeatherOverlay';
import { useWebSocket } from '../../hooks/useWebSocket';
import { api } from '../../lib/api';

interface Aircraft {
  id: string;
  registration: string;
  type: string;
  icaoType: string;
  callsign: string;
  origin: string;
  destination: string;
  status: 'planned' | 'scheduled' | 'active' | 'arrived' | 'diverted';
  latitude?: number;
  longitude?: number;
  altitude?: number;
  groundSpeed?: number;
  track?: number;
  eta?: string;
}

interface FleetDashboardProps {
  className?: string;
}

const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

export const FleetDashboard: React.FC<FleetDashboardProps> = ({ className = '' }) => {
  const mapContainer = useRef<HTMLDivElement>(null);
  const map = useRef<mapboxgl.Map | null>(null);
  const markersRef = useRef<Map<string, mapboxgl.Marker>>(new Map());
  const { orgId } = useAuth();
  const queryClient = useQueryClient();

  const [selectedAircraft, setSelectedAircraft] = useState<Aircraft | null>(null);
  const [showWeatherOverlay, setShowWeatherOverlay] = useState(false);
  const [mapStyle, setMapStyle] = useState('mapbox://styles/mapbox/light-v11');

  // Fetch fleet data
  const { data: fleet, isLoading } = useQuery({
    queryKey: ['fleet', orgId],
    queryFn: async () => {
      const response = await api.get('/fleet', {
        params: { operator_id: orgId },
      });
      return response.data as Aircraft[];
    },
    refetchInterval: 30000, // Refetch every 30 seconds as fallback
  });

  // Initialize map
  useEffect(() => {
    if (!mapContainer.current || map.current) return;

    mapboxgl.accessToken = MAPBOX_TOKEN;

    map.current = new mapboxgl.Map({
      container: mapContainer.current,
      style: mapStyle,
      center: [2.1686, 41.3874], // Default: Barcelona
      zoom: 6,
      attributionControl: false,
    });

    map.current.addControl(new mapboxgl.NavigationControl(), 'top-right');
    map.current.addControl(new mapboxgl.FullscreenControl(), 'top-right');
    map.current.addControl(
      new mapboxgl.AttributionControl({ compact: true }),
      'bottom-right'
    );

    return () => {
      map.current?.remove();
      map.current = null;
    };
  }, []);

  // Update map style
  useEffect(() => {
    if (map.current) {
      map.current.setStyle(mapStyle);
    }
  }, [mapStyle]);

  // Handle real-time updates via WebSocket
  const handleWebSocketMessage = useCallback((message: any) => {
    if (message.type === 'position' && message.data) {
      const position = message.data;

      // Update fleet data in cache
      queryClient.setQueryData(['fleet', orgId], (oldData: Aircraft[] | undefined) => {
        if (!oldData) return oldData;

        return oldData.map(aircraft => {
          if (aircraft.id === position.aircraft_id) {
            return {
              ...aircraft,
              latitude: position.latitude,
              longitude: position.longitude,
              altitude: position.altitude,
              groundSpeed: position.ground_speed,
              track: position.track,
            };
          }
          return aircraft;
        });
      });
    }
  }, [orgId, queryClient]);

  useWebSocket('/ws/fleet-updates', handleWebSocketMessage);

  // Update aircraft markers
  useEffect(() => {
    if (!map.current || !fleet) return;

    const currentMarkers = markersRef.current;
    const processedAircraft = new Set<string>();

    fleet.forEach(aircraft => {
      processedAircraft.add(aircraft.id);

      if (!aircraft.latitude || !aircraft.longitude) {
        // Remove marker if no position
        if (currentMarkers.has(aircraft.id)) {
          currentMarkers.get(aircraft.id)!.remove();
          currentMarkers.delete(aircraft.id);
        }
        return;
      }

      const el = document.createElement('div');
      el.innerHTML = AircraftMarker({ aircraft, isSelected: selectedAircraft?.id === aircraft.id });
      el.className = 'aircraft-marker';
      el.style.cursor = 'pointer';

      // Create or update marker
      if (currentMarkers.has(aircraft.id)) {
        const marker = currentMarkers.get(aircraft.id)!;
        marker.setLngLat([aircraft.longitude, aircraft.latitude]);
        marker.setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML(createPopupContent(aircraft)));
      } else {
        const marker = new mapboxgl.Marker(el)
          .setLngLat([aircraft.longitude, aircraft.latitude])
          .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML(createPopupContent(aircraft)))
          .addTo(map.current!);

        el.addEventListener('click', () => setSelectedAircraft(aircraft));
        currentMarkers.set(aircraft.id, marker);
      }
    });

    // Remove markers for aircraft no longer in fleet
    currentMarkers.forEach((marker, id) => {
      if (!processedAircraft.has(id)) {
        marker.remove();
        currentMarkers.delete(id);
      }
    });
  }, [fleet, selectedAircraft]);

  // Fit map to show all aircraft
  const fitToFleet = useCallback(() => {
    if (!map.current || !fleet || fleet.length === 0) return;

    const activeAircraft = fleet.filter(a => a.latitude && a.longitude);
    if (activeAircraft.length === 0) return;

    const bounds = new mapboxgl.LngLatBounds();
    activeAircraft.forEach(aircraft => {
      bounds.extend([aircraft.longitude!, aircraft.latitude!]);
    });

    map.current.fitBounds(bounds, { padding: 50 });
  }, [fleet]);

  const createPopupContent = (aircraft: Aircraft): string => {
    return `
      <div class="p-2">
        <h3 class="font-bold text-sm">${aircraft.callsign || aircraft.registration}</h3>
        <p class="text-xs text-gray-600">${aircraft.type}</p>
        <div class="mt-2 text-xs">
          <div>FL${Math.round((aircraft.altitude || 0) / 100)}</div>
          <div>${aircraft.groundSpeed}kts</div>
          <div>${aircraft.origin} → ${aircraft.destination}</div>
          ${aircraft.eta ? `<div>ETA: ${new Date(aircraft.eta).toLocaleTimeString()}</div>` : ''}
        </div>
      </div>
    `;
  };

  return (
    <div className={`relative h-full ${className}`}>
      {/* Map Container */}
      <div ref={mapContainer} className="absolute inset-0" />

      {/* Loading Overlay */}
      {isLoading && (
        <div className="absolute inset-0 bg-white/50 flex items-center justify-center z-10">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
        </div>
      )}

      {/* Controls Overlay */}
      <div className="absolute top-4 left-4 z-10 space-y-2">
        <button
          onClick={fitToFleet}
          className="bg-white px-3 py-2 rounded shadow hover:bg-gray-50 text-sm font-medium"
        >
          Fit to Fleet
        </button>

        <button
          onClick={() => setShowWeatherOverlay(!showWeatherOverlay)}
          className={`block w-full px-3 py-2 rounded shadow text-sm font-medium ${
            showWeatherOverlay ? 'bg-blue-600 text-white' : 'bg-white hover:bg-gray-50'
          }`}
        >
          Weather Overlay
        </button>

        <select
          value={mapStyle}
          onChange={(e) => setMapStyle(e.target.value)}
          className="block w-full px-3 py-2 rounded shadow text-sm bg-white"
        >
          <option value="mapbox://styles/mapbox/light-v11">Light</option>
          <option value="mapbox://styles/mapbox/dark-v11">Dark</option>
          <option value="mapbox://styles/mapbox/satellite-v9">Satellite</option>
          <option value="mapbox://styles/mapbox/navigation-day-v1">Navigation</option>
        </select>
      </div>

      {/* Weather Overlay */}
      {showWeatherOverlay && map.current && (
        <WeatherOverlay map={map.current} visible={showWeatherOverlay} />
      )}

      {/* Fleet Stats */}
      {fleet && (
        <div className="absolute bottom-4 left-4 z-10 bg-white/90 backdrop-blur rounded-lg p-3 shadow">
          <div className="text-sm font-medium">
            Fleet Status
          </div>
          <div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
            <span className="text-gray-600">Active:</span>
            <span className="font-medium">{fleet.filter(a => a.status === 'active').length}</span>
            <span className="text-gray-600">Scheduled:</span>
            <span className="font-medium">{fleet.filter(a => a.status === 'scheduled').length}</span>
            <span className="text-gray-600">Total:</span>
            <span className="font-medium">{fleet.length}</span>
          </div>
        </div>
      )}
    </div>
  );
};

// AircraftMarker component
const AircraftMarker: React.FC<{ aircraft: Aircraft; isSelected: boolean }> = ({ 
  aircraft, 
  isSelected 
}) => {
  const getStatusColor = (status: string) => {
    switch (status) {
      case 'active': return '#22c55e'; // green
      case 'scheduled': return '#3b82f6'; // blue
      case 'arrived': return '#6b7280'; // gray
      case 'diverted': return '#ef4444'; // red
      default: return '#9ca3af';
    }
  };

  const color = getStatusColor(aircraft.status);
  const rotation = aircraft.track || 0;

  return (
    <div 
      className={`relative transition-transform duration-300 ${isSelected ? 'scale-125' : ''}`}
      style={{ transform: `rotate(${rotation}deg)` }}
    >
      {/* Aircraft icon (simplified triangle) */}
      <svg 
        width="24" 
        height="24" 
        viewBox="0 0 24 24" 
        fill="none"
        className="drop-shadow-md"
      >
        <path
          d="M12 2L14 10L22 12L14 14L12 22L10 14L2 12L10 10L12 2Z"
          fill={color}
          stroke="white"
          strokeWidth="1.5"
        />
      </svg>

      {/* Status indicator */}
      {aircraft.status === 'active' && (
        <span className="absolute -top-1 -right-1 flex h-2 w-2">
          <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
          <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
        </span>
      )}
    </div>
  );
};

B. Fleet List Component with Filtering

// frontend/src/components/fleet/FleetList.tsx
import React, { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '@clerk/clerk-react';
import { api } from '../../lib/api';
import { Pagination } from '../ui/Pagination';
import { Badge } from '../ui/Badge';
import { Input } from '../ui/Input';
import { Select } from '../ui/Select';

interface Aircraft {
  id: string;
  registration: string;
  type: string;
  icaoType: string;
  callsign: string;
  origin: string;
  destination: string;
  status: 'planned' | 'scheduled' | 'active' | 'arrived' | 'diverted';
  latitude?: number;
  longitude?: number;
  altitude?: number;
  groundSpeed?: number;
  eta?: string;
}

const ITEMS_PER_PAGE = 10;

export const FleetList: React.FC = () => {
  const { orgId } = useAuth();
  const [currentPage, setCurrentPage] = useState(1);
  const [searchQuery, setSearchQuery] = useState('');
  const [typeFilter, setTypeFilter] = useState<string>('all');
  const [statusFilter, setStatusFilter] = useState<string>('all');

  const { data: fleet, isLoading } = useQuery({
    queryKey: ['fleet', orgId],
    queryFn: async () => {
      const response = await api.get('/fleet', {
        params: { operator_id: orgId },
      });
      return response.data as Aircraft[];
    },
  });

  // Get unique aircraft types for filter dropdown
  const aircraftTypes = useMemo(() => {
    if (!fleet) return [];
    const types = new Set(fleet.map(a => a.type));
    return Array.from(types).sort();
  }, [fleet]);

  // Filter and paginate
  const filteredFleet = useMemo(() => {
    if (!fleet) return [];

    return fleet.filter(aircraft => {
      // Search filter (registration or callsign)
      const matchesSearch = !searchQuery || 
        aircraft.registration.toLowerCase().includes(searchQuery.toLowerCase()) ||
        (aircraft.callsign && aircraft.callsign.toLowerCase().includes(searchQuery.toLowerCase()));

      // Type filter
      const matchesType = typeFilter === 'all' || aircraft.type === typeFilter;

      // Status filter
      const matchesStatus = statusFilter === 'all' || aircraft.status === statusFilter;

      return matchesSearch && matchesType && matchesStatus;
    });
  }, [fleet, searchQuery, typeFilter, statusFilter]);

  const totalPages = Math.ceil(filteredFleet.length / ITEMS_PER_PAGE);
  const paginatedFleet = filteredFleet.slice(
    (currentPage - 1) * ITEMS_PER_PAGE,
    currentPage * ITEMS_PER_PAGE
  );

  const getStatusColor = (status: string) => {
    switch (status) {
      case 'active': return 'green';
      case 'scheduled': return 'blue';
      case 'arrived': return 'gray';
      case 'diverted': return 'red';
      default: return 'gray';
    }
  };

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {/* Filters */}
      <div className="bg-white p-4 rounded-lg shadow-sm border space-y-4">
        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
          {/* Search */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Search Registration/Callsign
            </label>
            <Input
              type="text"
              placeholder="e.g., EC-ABC or IBE123..."
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              className="w-full"
            />
          </div>

          {/* Type Filter */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Aircraft Type
            </label>
            <Select
              value={typeFilter}
              onChange={(e) => setTypeFilter(e.target.value)}
              className="w-full"
            >
              <option value="all">All Types</option>
              {aircraftTypes.map(type => (
                <option key={type} value={type}>{type}</option>
              ))}
            </Select>
          </div>

          {/* Status Filter */}
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Status
            </label>
            <Select
              value={statusFilter}
              onChange={(e) => setStatusFilter(e.target.value)}
              className="w-full"
            >
              <option value="all">All Status</option>
              <option value="active">Active</option>
              <option value="scheduled">Scheduled</option>
              <option value="arrived">Arrived</option>
              <option value="diverted">Diverted</option>
            </Select>
          </div>
        </div>

        {/* Results count */}
        <div className="text-sm text-gray-600">
          Showing {filteredFleet.length} aircraft
          {filteredFleet.length !== (fleet?.length || 0) && ` (filtered from ${fleet?.length})`}
        </div>
      </div>

      {/* Aircraft Grid */}
      <div className="grid grid-cols-1 gap-4">
        {paginatedFleet.map(aircraft => (
          <div 
            key={aircraft.id}
            className="bg-white p-4 rounded-lg shadow-sm border hover:shadow-md transition-shadow"
          >
            <div className="flex items-start justify-between">
              {/* Left: Aircraft Info */}
              <div className="flex-1">
                <div className="flex items-center gap-3">
                  <h3 className="text-lg font-semibold text-gray-900">
                    {aircraft.registration}
                  </h3>
                  <Badge color={getStatusColor(aircraft.status)}>
                    {aircraft.status.charAt(0).toUpperCase() + aircraft.status.slice(1)}
                  </Badge>
                  {aircraft.callsign && (
                    <span className="text-sm text-gray-500">
                      {aircraft.callsign}
                    </span>
                  )}
                </div>

                <p className="text-sm text-gray-600 mt-1">
                  {aircraft.type} ({aircraft.icaoType})
                </p>

                {/* Route */}
                <div className="flex items-center gap-2 mt-3 text-sm">
                  <span className="font-medium">{aircraft.origin}</span>
                  <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
                  </svg>
                  <span className="font-medium">{aircraft.destination}</span>
                </div>

                {/* Position Details (if active) */}
                {aircraft.status === 'active' && aircraft.latitude && (
                  <div className="mt-3 grid grid-cols-3 gap-4 text-sm">
                    <div>
                      <span className="text-gray-500">Altitude</span>
                      <p className="font-medium">FL{Math.round((aircraft.altitude || 0) / 100)}</p>
                    </div>
                    <div>
                      <span className="text-gray-500">Speed</span>
                      <p className="font-medium">{aircraft.groundSpeed}kts</p>
                    </div>
                    <div>
                      <span className="text-gray-500">ETA</span>
                      <p className="font-medium">
                        {aircraft.eta ? new Date(aircraft.eta).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '--:--'}
                      </p>
                    </div>
                  </div>
                )}
              </div>

              {/* Right: Actions */}
              <div className="flex flex-col gap-2">
                <button 
                  onClick={() => {/* Navigate to flight detail */}}
                  className="text-sm text-blue-600 hover:text-blue-800 font-medium"
                >
                  View Details
                </button>
                {aircraft.status === 'active' && (
                  <button 
                    onClick={() => {/* Focus map on aircraft */}}
                    className="text-sm text-gray-600 hover:text-gray-800"
                  >
                    Track Live
                  </button>
                )}
              </div>
            </div>
          </div>
        ))}
      </div>

      {/* Empty State */}
      {paginatedFleet.length === 0 && !isLoading && (
        <div className="text-center py-12 bg-gray-50 rounded-lg">
          <svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
          </svg>
          <h3 className="mt-2 text-sm font-medium text-gray-900">No aircraft found</h3>
          <p className="mt-1 text-sm text-gray-500">
            {searchQuery || typeFilter !== 'all' || statusFilter !== 'all'
              ? 'Try adjusting your filters'
              : 'No aircraft in your fleet'}
          </p>
        </div>
      )}

      {/* Pagination */}
      {totalPages > 1 && (
        <Pagination
          currentPage={currentPage}
          totalPages={totalPages}
          onPageChange={setCurrentPage}
        />
      )}
    </div>
  );
};

C. Alert Components

// frontend/src/components/alerts/AlertNotification.tsx
import React, { useState, useEffect } from 'react';
import { useAuth } from '@clerk/clerk-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../../lib/api';
import { useWebSocket } from '../../hooks/useWebSocket';
import { formatDistanceToNow } from 'date-fns';

interface Alert {
  id: string;
  type: 'diversion' | 'arrival' | 'departure' | 'cancellation' | 'delay' | 'weather_enroute' | 'eta_change';
  severity: 'low' | 'medium' | 'high' | 'critical';
  title: string;
  message: string;
  callsign: string;
  origin: string;
  destination: string;
  createdAt: string;
  acknowledgedAt?: string;
}

export const AlertNotification: React.FC = () => {
  const { orgId } = useAuth();
  const queryClient = useQueryClient();
  const [isOpen, setIsOpen] = useState(false);
  const [unreadCount, setUnreadCount] = useState(0);

  const { data: alerts, refetch } = useQuery({
    queryKey: ['alerts', orgId],
    queryFn: async () => {
      const response = await api.get('/alerts', {
        params: { 
          operator_id: orgId, 
          unacknowledged_only: true,
          limit: 50 
        },
      });
      return response.data as Alert[];
    },
    refetchInterval: 30000,
  });

  // Update unread count
  useEffect(() => {
    if (alerts) {
      setUnreadCount(alerts.filter(a => !a.acknowledgedAt).length);
    }
  }, [alerts]);

  // Handle real-time alert updates
  const handleWebSocketMessage = (message: any) => {
    if (['diversion', 'arrival', 'departure', 'cancellation', 'delay', 'weather_enroute', 'eta_change'].includes(message.type)) {
      // Play notification sound for critical alerts
      if (message.severity === 'critical') {
        new Audio('/sounds/alert-critical.mp3').play().catch(() => {});
      }

      // Refetch alerts
      refetch();
    }
  };

  useWebSocket('/ws/fleet-updates', handleWebSocketMessage);

  const acknowledgeMutation = useMutation({
    mutationFn: async (alertId: string) => {
      await api.post(`/alerts/${alertId}/acknowledge`);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['alerts', orgId] });
    },
  });

  const getSeverityColor = (severity: string) => {
    switch (severity) {
      case 'critical': return 'bg-red-100 text-red-800 border-red-300';
      case 'high': return 'bg-orange-100 text-orange-800 border-orange-300';
      case 'medium': return 'bg-yellow-100 text-yellow-800 border-yellow-300';
      default: return 'bg-blue-100 text-blue-800 border-blue-300';
    }
  };

  const getAlertIcon = (type: string) => {
    switch (type) {
      case 'diversion':
        return (
          <svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
          </svg>
        );
      case 'arrival':
        return (
          <svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
          </svg>
        );
      case 'departure':
        return (
          <svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
          </svg>
        );
      default:
        return (
          <svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
        );
    }
  };

  return (
    <div className="relative">
      {/* Alert Bell Button */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="relative p-2 text-gray-600 hover:text-gray-900 focus:outline-none"
      >
        <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
        </svg>

        {unreadCount > 0 && (
          <span className="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/4 -translate-y-1/4 bg-red-600 rounded-full">
            {unreadCount > 99 ? '99+' : unreadCount}
          </span>
        )}
      </button>

      {/* Alert Dropdown */}
      {isOpen && (
        <>
          <div 
            className="fixed inset-0 z-10" 
            onClick={() => setIsOpen(false)} 
          />
          <div className="absolute right-0 mt-2 w-96 bg-white rounded-lg shadow-lg border z-20 max-h-[600px] overflow-hidden">
            <div className="p-4 border-b flex items-center justify-between">
              <h3 className="font-semibold text-gray-900">Alerts</h3>
              {unreadCount > 0 && (
                <span className="text-sm text-gray-500">
                  {unreadCount} unread
                </span>
              )}
            </div>

            <div className="overflow-y-auto max-h-[500px]">
              {!alerts || alerts.length === 0 ? (
                <div className="p-8 text-center text-gray-500">
                  <svg className="mx-auto h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
                  </svg>
                  <p className="mt-2">No active alerts</p>
                </div>
              ) : (
                <div className="divide-y divide-gray-100">
                  {alerts.map(alert => (
                    <div 
                      key={alert.id} 
                      className={`p-4 hover:bg-gray-50 transition-colors ${
                        !alert.acknowledgedAt ? 'bg-blue-50/50' : ''
                      }`}
                    >
                      <div className="flex items-start gap-3">
                        <div className="flex-shrink-0 mt-0.5">
                          {getAlertIcon(alert.type)}
                        </div>

                        <div className="flex-1 min-w-0">
                          <div className="flex items-center gap-2">
                            <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getSeverityColor(alert.severity)}`}>
                              {alert.severity.toUpperCase()}
                            </span>
                            <span className="text-xs text-gray-500">
                              {formatDistanceToNow(new Date(alert.createdAt), { addSuffix: true })}
                            </span>
                          </div>

                          <h4 className="mt-1 text-sm font-medium text-gray-900">
                            {alert.title}
                          </h4>

                          <p className="mt-1 text-sm text-gray-600">
                            {alert.message}
                          </p>

                          <p className="mt-1 text-xs text-gray-500">
                            {alert.callsign} • {alert.origin} → {alert.destination}
                          </p>

                          {!alert.acknowledgedAt && (
                            <button
                              onClick={() => acknowledgeMutation.mutate(alert.id)}
                              disabled={acknowledgeMutation.isPending}
                              className="mt-2 text-xs text-blue-600 hover:text-blue-800 font-medium disabled:opacity-50"
                            >
                              {acknowledgeMutation.isPending ? 'Acknowledging...' : 'Acknowledge'}
                            </button>
                          )}
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>

            <div className="p-3 border-t bg-gray-50">
              <a 
                href="/alerts" 
                className="block text-center text-sm text-blue-600 hover:text-blue-800 font-medium"
              >
                View All Alerts
              </a>
            </div>
          </div>
        </>
      )}
    </div>
  );
};

// Alert History Page
export const AlertHistory: React.FC = () => {
  const { orgId } = useAuth();
  const [filter, setFilter] = useState({
    type: 'all',
    severity: 'all',
    acknowledged: 'all',
  });

  const { data: alerts, isLoading } = useQuery({
    queryKey: ['alerts', 'history', orgId, filter],
    queryFn: async () => {
      const params: any = { operator_id: orgId, limit: 100 };
      if (filter.type !== 'all') params.type = filter.type;
      if (filter.severity !== 'all') params.severity = filter.severity;
      if (filter.acknowledged !== 'all') {
        params.unacknowledged_only = filter.acknowledged === 'unacknowledged';
      }

      const response = await api.get('/alerts', { params });
      return response.data as Alert[];
    },
  });

  return (
    <div className="max-w-6xl mx-auto p-6">
      <h1 className="text-2xl font-bold text-gray-900 mb-6">Alert History</h1>

      {/* Filters */}
      <div className="bg-white p-4 rounded-lg shadow-sm border mb-6">
        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
          <select
            value={filter.type}
            onChange={(e) => setFilter(f => ({ ...f, type: e.target.value }))}
            className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          >
            <option value="all">All Types</option>
            <option value="diversion">Diversion</option>
            <option value="arrival">Arrival</option>
            <option value="departure">Departure</option>
            <option value="delay">Delay</option>
            <option value="weather_enroute">Weather</option>
            <option value="eta_change">ETA Change</option>
          </select>

          <select
            value={filter.severity}
            onChange={(e) => setFilter(f => ({ ...f, severity: e.target.value }))}
            className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          >
            <option value="all">All Severities</option>
            <option value="critical">Critical</option>
            <option value="high">High</option>
            <option value="medium">Medium</option>
            <option value="low">Low</option>
          </select>

          <select
            value={filter.acknowledged}
            onChange={(e) => setFilter(f => ({ ...f, acknowledged: e.target.value }))}
            className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          >
            <option value="all">All Status</option>
            <option value="acknowledged">Acknowledged</option>
            <option value="unacknowledged">Unacknowledged</option>
          </select>
        </div>
      </div>

      {/* Alert List */}
      {isLoading ? (
        <div className="flex justify-center py-12">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
        </div>
      ) : (
        <div className="bg-white rounded-lg shadow overflow-hidden">
          <table className="min-w-full divide-y divide-gray-200">
            <thead className="bg-gray-50">
              <tr>
                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                  Type
                </th>
                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                  Flight
                </th>
                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                  Message
                </th>
                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                  Time
                </th>
                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                  Status
                </th>
              </tr>
            </thead>
            <tbody className="bg-white divide-y divide-gray-200">
              {alerts?.map(alert => (
                <tr key={alert.id} className="hover:bg-gray-50">
                  <td className="px-6 py-4 whitespace-nowrap">
                    <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
                      alert.severity === 'critical' ? 'bg-red-100 text-red-800' :
                      alert.severity === 'high' ? 'bg-orange-100 text-orange-800' :
                      alert.severity === 'medium' ? 'bg-yellow-100 text-yellow-800' :
                      'bg-blue-100 text-blue-800'
                    }`}>
                      {alert.type.replace('_', ' ')}
                    </span>
                  </td>
                  <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                    {alert.callsign}
                  </td>
                  <td className="px-6 py-4 text-sm text-gray-600 max-w-md">
                    {alert.message}
                  </td>
                  <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                    {new Date(alert.createdAt).toLocaleString()}
                  </td>
                  <td className="px-6 py-4 whitespace-nowrap">
                    {alert.acknowledgedAt ? (
                      <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
                        Acknowledged
                      </span>
                    ) : (
                      <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
                        Pending
                      </span>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
};

5. API Endpoints

REST API Specification

# Tracking Module API Endpoints

# ============================================================================
# FLEET ENDPOINTS
# ============================================================================

GET /api/v1/fleet
  Description: List all aircraft in operator's fleet with current positions
  Authentication: Required (JWT + Org ID)
  Query Parameters:
    - status: Filter by status (planned, scheduled, active, arrived, diverted)
    - type: Filter by aircraft type
    - registration: Search by registration
    - page: Page number (default: 1)
    - per_page: Items per page (default: 10, max: 100)
  Response: 200 OK
    {
      "data": [
        {
          "id": "uuid",
          "registration": "EC-ABC",
          "type": "Cessna Citation X",
          "icao_type": "C56X",
          "callsign": "IBE123",
          "origin": "LEMD",
          "destination": "LEBL",
          "status": "active",
          "latitude": 41.385,
          "longitude": 2.173,
          "altitude": 35000,
          "ground_speed": 450,
          "track": 90,
          "eta": "2024-01-15T14:30:00Z",
          "last_updated": "2024-01-15T13:45:00Z"
        }
      ],
      "meta": {
        "current_page": 1,
        "total_pages": 3,
        "total_count": 25
      }
    }

# ============================================================================
# AIRCRAFT POSITION ENDPOINTS
# ============================================================================

GET /api/v1/aircraft/{id}/position
  Description: Get current position for a specific aircraft
  Authentication: Required
  Path Parameters:
    - id: Aircraft UUID
  Response: 200 OK
    {
      "id": "uuid",
      "aircraft_id": "uuid",
      "flight_id": "uuid",
      "latitude": 41.385,
      "longitude": 2.173,
      "altitude": 35000,
      "ground_speed": 450,
      "track": 90,
      "vertical_speed": 0,
      "timestamp": "2024-01-15T13:45:00Z",
      "source": "flightaware"
    }
  Errors:
    404: Aircraft not found or no position data

# ============================================================================
# FLIGHT TRACK ENDPOINTS
# ============================================================================

GET /api/v1/flights/{id}/track
  Description: Get full flight track (position history)
  Authentication: Required
  Path Parameters:
    - id: Flight UUID
  Query Parameters:
    - format: Response format (json, geojson, kml) - default: json
  Response: 200 OK
    {
      "flight_id": "uuid",
      "fa_flight_id": "string",
      "start_time": "2024-01-15T12:00:00Z",
      "end_time": "2024-01-15T14:30:00Z",
      "point_count": 150,
      "positions": [
        {
          "latitude": 41.385,
          "longitude": 2.173,
          "altitude": 35000,
          "ground_speed": 450,
          "track": 90,
          "timestamp": "2024-01-15T13:45:00Z"
        }
      ]
    }

GET /api/v1/flights/{id}/track/download
  Description: Download flight track as file
  Authentication: Required
  Query Parameters:
    - format: File format (kml, gpx, csv)
  Response: File download

# ============================================================================
# FLIGHT ALERT ENDPOINTS
# ============================================================================

GET /api/v1/flights/{id}/alerts
  Description: Get alerts for a specific flight
  Authentication: Required
  Query Parameters:
    - unacknowledged_only: boolean
  Response: 200 OK
    {
      "data": [
        {
          "id": "uuid",
          "type": "weather_enroute",
          "severity": "high",
          "title": "SIGMET Active Along Route",
          "message": "SIGMET for severe turbulence...",
          "created_at": "2024-01-15T13:30:00Z",
          "acknowledged_at": null,
          "acknowledged_by": null
        }
      ]
    }

# ============================================================================
# GLOBAL ALERT ENDPOINTS
# ============================================================================

GET /api/v1/alerts
  Description: List all alerts for operator
  Authentication: Required
  Query Parameters:
    - type: Filter by alert type
    - severity: Filter by severity
    - flight_id: Filter by flight
    - unacknowledged_only: boolean
    - page: Page number
    - per_page: Items per page
  Response: 200 OK
    {
      "data": [/* Alert objects */],
      "meta": { /* Pagination info */ }
    }

POST /api/v1/alerts/{id}/acknowledge
  Description: Acknowledge an alert
  Authentication: Required
  Path Parameters:
    - id: Alert UUID
  Response: 200 OK
    {
      "id": "uuid",
      "acknowledged_at": "2024-01-15T14:00:00Z",
      "acknowledged_by": "user_uuid"
    }

# ============================================================================
# TRACKING CONTROL ENDPOINTS
# ============================================================================

POST /api/v1/flights/{id}/tracking/start
  Description: Start tracking a flight
  Authentication: Required (admin, dispatcher roles)
  Request Body:
    {
      "fa_flight_id": "flight_aware_id" // Optional - will search if not provided
    }
  Response: 201 Created
    {
      "tracked_flight_id": "uuid",
      "status": "tracking_started",
      "fa_flight_id": "string"
    }

POST /api/v1/flights/{id}/tracking/stop
  Description: Stop tracking a flight
  Authentication: Required (admin, dispatcher roles)
  Response: 200 OK
    {
      "status": "tracking_stopped"
    }

# ============================================================================
# PREDICTION ENDPOINTS (Foresight)
# ============================================================================

GET /api/v1/flights/{id}/prediction
  Description: Get Foresight ETA prediction
  Authentication: Required
  Response: 200 OK
    {
      "flight_id": "uuid",
      "predicted_eta": "2024-01-15T14:25:00Z",
      "confidence": 0.92,
      "trend": "early",
      "minutes_offset": -5
    }

# ============================================================================
# WEBSOCKET ENDPOINT
# ============================================================================

WS /ws/fleet-updates
  Description: Real-time fleet updates via WebSocket
  Authentication: Required (JWT in query param or header)
  Protocol: Socket.io or native WebSocket

  Messages:
    Client → Server:
      - subscribe: { "operator_id": "uuid" }
      - ping: {}

    Server → Client:
      - position: { "type": "position", "data": AircraftPosition }
      - departure: { "type": "departure", "flight_id": "uuid", "timestamp": "..." }
      - arrival: { "type": "arrival", "flight_id": "uuid", "destination": "ICAO" }
      - diversion: { "type": "diversion", "flight_id": "uuid", "severity": "critical", "diversion_airport": "ICAO" }
      - alert: { "type": "alert", "alert": AlertObject }
      - eta_change: { "type": "eta_change", "flight_id": "uuid", "delta_minutes": -5 }

# ============================================================================
# WEBHOOK ENDPOINTS (Internal)
# ============================================================================

POST /api/v1/webhooks/flightaware
  Description: Receive webhooks from FlightAware
  Authentication: HMAC Signature verification
  Headers:
    - X-FlightAware-Signature: sha256=<hex>
  Body: FlightAware webhook payload
  Response: 200 OK (always return 200 to acknowledge receipt)

6. Multi-tenancy & Security

Operator Isolation

All tracking data is scoped to the operator level. The system enforces multi-tenancy at multiple layers:

Database Level

-- Every query automatically filters by operator_id
SELECT * FROM tracked_flights WHERE operator_id = $1;
SELECT * FROM aircraft_positions WHERE operator_id = $1;
SELECT * FROM tracking_alerts WHERE operator_id = $1;

Application Level (Go Middleware)

// internal/adapters/http/middleware/tenant.go

func TenantMiddleware() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // Extract org_id from JWT claims (set by auth middleware)
            orgID, ok := c.Get("org_id").(string)
            if !ok {
                return echo.NewHTTPError(401, "Organization context required")
            }

            // Parse as UUID
            operatorID, err := uuid.Parse(orgID)
            if err != nil {
                return echo.NewHTTPError(400, "Invalid organization ID")
            }

            // Add to context
            c.Set("operator_id", operatorID)

            return next(c)
        }
    }
}

// Usage in handlers
func (h *FleetHandler) GetFleet(c echo.Context) error {
    operatorID := c.Get("operator_id").(uuid.UUID)

    // All queries automatically scoped to operator_id
    flights, err := h.service.ListTrackedFlights(c.Request().Context(), operatorID, nil)
    if err != nil {
        return err
    }

    return c.JSON(200, flights)
}

JWT Claims Structure

{
  "sub": "user_uuid",
  "org_id": "operator_uuid",
  "org_role": "admin",
  "org_permissions": ["flights:create", "flights:read", "tracking:write"],
  "iat": 1705315200,
  "exp": 1705909999
}

Role-Based Access Control

// Permissions for tracking module
const (
    PermFleetRead       = "fleet:read"
    PermFleetWrite      = "fleet:write"
    PermTrackingControl = "tracking:control"  // Start/stop tracking
    PermAlertsRead      = "alerts:read"
    PermAlertsWrite     = "alerts:write"      // Acknowledge alerts
)

// Role permissions matrix
var rolePermissions = map[string][]string{
    "org:admin": {
        PermFleetRead, PermFleetWrite, 
        PermTrackingControl,
        PermAlertsRead, PermAlertsWrite,
    },
    "org:chiefpilot": {
        PermFleetRead, PermFleetWrite,
        PermTrackingControl,
        PermAlertsRead, PermAlertsWrite,
    },
    "org:dispatcher": {
        PermFleetRead,
        PermTrackingControl,
        PermAlertsRead,
    },
    "org:pilot": {
        PermFleetRead,
        PermAlertsRead,
    },
    "org:readonly": {
        PermFleetRead,
        PermAlertsRead,
    },
}

Webhook Security

// HMAC Signature Verification
func VerifyWebhookSignature(payload []byte, signature string, secret string) error {
    const prefix = "sha256="
    if !strings.HasPrefix(signature, prefix) {
        return errors.New("invalid signature format")
    }

    expectedSig := signature[len(prefix):]

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    computedSig := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expectedSig), []byte(computedSig)) {
        return errors.New("signature mismatch")
    }

    return nil
}

// Webhook endpoint security headers
// - TLS 1.3 required
// - IP allowlist (FlightAware egress IPs)
// - Request timestamp validation (prevent replay attacks)

Data Retention & Compliance

Data Type Retention Reason
Aircraft Positions 24 hours Real-time tracking only
Flight Tracks 12 months EASA compliance audit
Flight Status Events 12 months Audit trail
Tracking Alerts 12 months Incident investigation
Weather Correlation 7 days Temporary analysis

7. Integration Points

Weather Service Correlation

// Integration with existing weather service

func (s *TrackingService) CorrelateWeather(ctx context.Context, position *AircraftPosition) error {
    // Find nearest airports with METAR
    airports, err := s.airportRepo.FindNearest(ctx, position.Latitude, position.Longitude, 3)
    if err != nil {
        return err
    }

    // Fetch METARs
    var metars []weather.METAR
    for _, airport := range airports {
        metar, err := s.weatherService.GetMETAR(ctx, airport.ICAO)
        if err != nil {
            s.logger.Error("Failed to fetch METAR", "icao", airport.ICAO, "error", err)
            continue
        }
        metars = append(metars, *metar)
    }

    // Check for SIGMETs in area
    sigmets, err := s.weatherService.GetSIGMETsForArea(ctx, 
        position.Latitude, 
        position.Longitude, 
        100, // 100nm radius
    )
    if err != nil {
        s.logger.Error("Failed to fetch SIGMETs", "error", err)
    }

    // Generate alerts if needed
    if len(sigmets) > 0 {
        alert := &ports.TrackingAlert{
            FlightID:   position.FlightID,
            Type:       ports.AlertTypeWeatherEnRoute,
            Severity:   "high",
            Title:      "SIGMET Active",
            Message:    fmt.Sprintf("SIGMET for %s in vicinity", sigmets[0].Phenomenon),
            Source:     "weather_correlation",
        }
        s.alertRepo.SaveAlert(ctx, alert)
    }

    return nil
}

Compliance Report Enhancement

// Add tracking data to compliance reports

func (s *ComplianceService) GenerateReportWithTracking(ctx context.Context, flightID uuid.UUID) (*ComplianceReport, error) {
    // Get standard compliance data
    report, err := s.generateBaseReport(ctx, flightID)
    if err != nil {
        return nil, err
    }

    // Add tracking data
    track, err := s.trackingRepo.GetTrack(ctx, flightID)
    if err == nil && track != nil {
        report.TrackingData = &TrackingData{
            ActualDeparture:   track.StartTime,
            ActualArrival:     track.EndTime,
            TrackPoints:       track.PointCount,
            RouteDeviation:    s.calculateDeviation(track),
            MaxAltitude:       s.getMaxAltitude(track),
            AverageSpeed:      s.getAverageSpeed(track),
        }
    }

    // Add any weather-related alerts during flight
    alerts, err := s.alertRepo.ListForFlight(ctx, flightID)
    if err == nil {
        report.InFlightAlerts = alerts
    }

    return report, nil
}

Alert Service Integration

// Send notifications via multiple channels

type AlertService struct {
    emailService    EmailService
    smsService      SMSService
    pushService     PushNotificationService
    websocketHub    *websocket.Hub
}

func (s *AlertService) SendAlert(ctx context.Context, alert *ports.TrackingAlert, operators []uuid.UUID) error {
    // In-app notification via WebSocket
    if s.websocketHub != nil {
        s.websocketHub.BroadcastToOperators(operators, websocket.Message{
            Type: "alert",
            Data: alert,
        })
    }

    // Email for critical alerts
    if alert.Severity == "critical" {
        for _, operatorID := range operators {
            users, err := s.userRepo.GetByOperatorWithRole(ctx, operatorID, "admin", "chiefpilot")
            if err != nil {
                continue
            }

            for _, user := range users {
                s.emailService.Send(ctx, user.Email, EmailTemplateCriticalAlert, alert)
            }
        }
    }

    // SMS for diversions
    if alert.Type == ports.AlertTypeDiversion {
        // Get phone numbers for on-call personnel
        phones, err := s.getEmergencyContacts(ctx, alert.OperatorID)
        if err == nil {
            for _, phone := range phones {
                s.smsService.Send(ctx, phone, fmt.Sprintf("DIVERSION ALERT: %s", alert.Message))
            }
        }
    }

    return nil
}

8. Configuration

Environment Variables

# =============================================================================
# FLIGHTAWARE AEROAPI CONFIGURATION
# =============================================================================

# Required: Your FlightAware AeroAPI Enterprise API key
FLIGHTAWARE_API_KEY=your_api_key_here

# Required: Webhook secret for HMAC signature verification
FLIGHTAWARE_WEBHOOK_SECRET=your_webhook_secret_here

# Optional: API base URL (default: https://aeroapi.flightaware.com/aeroapi)
FLIGHTAWARE_BASE_URL=https://aeroapi.flightaware.com/aeroapi

# Optional: Rate limit (requests per minute, default: 100)
FLIGHTAWARE_RATE_LIMIT=100

# Optional: Request timeout (default: 30s)
FLIGHTAWARE_TIMEOUT=30s

# =============================================================================
# TRACKING MODULE CONFIGURATION
# =============================================================================

# Polling interval for active flights (default: 60s)
TRACKING_POLL_INTERVAL=60s

# Weather correlation interval (default: 5m)
TRACKING_WEATHER_INTERVAL=5m

# Maximum concurrent API calls (default: 10)
TRACKING_MAX_CONCURRENT=10

# Position data TTL in Redis (default: 5m)
TRACKING_CACHE_TTL=5m

# Enable weather correlation (default: true)
TRACKING_ENABLE_WEATHER=true

# =============================================================================
# DATABASE CONFIGURATION (PostgreSQL)
# =============================================================================

# Connection string (already configured for main app)
# DATABASE_URL=postgresql://user:pass@localhost/skybrief

# Connection pool settings
DB_MAX_OPEN_CONNS=25
DB_MAX_IDLE_CONNS=5
DB_CONN_MAX_LIFETIME=5m

# =============================================================================
# REDIS CONFIGURATION (Caching)
# =============================================================================

# Redis URL (optional - falls back to in-memory cache)
REDIS_URL=redis://localhost:6379/0

# Redis connection pool
REDIS_POOL_SIZE=10

# =============================================================================
# WEBSOCKET CONFIGURATION
# =============================================================================

# WebSocket server port (default: 8080)
WS_PORT=8080

# WebSocket ping interval (default: 30s)
WS_PING_INTERVAL=30s

# Maximum connections per operator (default: 100)
WS_MAX_CONNECTIONS_PER_ORG=100

# =============================================================================
# WEBHOOK CONFIGURATION
# =============================================================================

# Public URL for FlightAware webhooks (must be HTTPS)
WEBHOOK_BASE_URL=https://api.skybrief.io

# Webhook endpoint path (default: /api/v1/webhooks/flightaware)
WEBHOOK_PATH=/api/v1/webhooks/flightaware

# =============================================================================
# MAPBOX CONFIGURATION
# =============================================================================

# Mapbox access token (for frontend)
VITE_MAPBOX_ACCESS_TOKEN=pk.your_mapbox_token_here

# =============================================================================
# SECURITY CONFIGURATION
# =============================================================================

# JWT secret (already configured for main app)
# JWT_SECRET=your_jwt_secret

# Webhook signature algorithm (default: sha256)
WEBHOOK_SIGNATURE_ALGO=sha256

# Allowed webhook IP ranges (FlightAware egress IPs)
WEBHOOK_ALLOWED_IPS=52.0.0.0/8,54.0.0.0/8

# Request timeout for external APIs
API_REQUEST_TIMEOUT=30s

# =============================================================================
# MONITORING & LOGGING
# =============================================================================

# Log level (debug, info, warn, error)
LOG_LEVEL=info

# Enable structured logging (JSON)
LOG_FORMAT=json

# Metrics endpoint enabled
METRICS_ENABLED=true
METRICS_PORT=9090

Configuration File (config/tracking.yaml)

# tracking.yaml - Tracking module configuration

flightaware:
  api_key: ${FLIGHTAWARE_API_KEY}
  webhook_secret: ${FLIGHTAWARE_WEBHOOK_SECRET}
  base_url: "https://aeroapi.flightaware.com/aeroapi"
  rate_limit: 100  # requests per minute
  timeout: 30s

  # Alert types to subscribe to
  alert_types:
    - departure
    - arrival
    - diversion
    - cancellation
    - delay
    - eta_change

tracking:
  poll_interval: 60s
  weather_interval: 5m
  max_concurrent: 10
  enable_weather: true

  # Automatic actions
  auto_start_tracking: true  # Start tracking when flight status changes to scheduled
  auto_stop_on_arrival: true  # Stop tracking after arrival

  # Position data retention
  position_ttl: 24h  # Database cleanup
  cache_ttl: 5m      # Redis cache

database:
  # Partition settings
  partition_months_ahead: 2  # Create partitions N months ahead

  # Cleanup jobs
  cleanup:
    positions:
      enabled: true
      interval: 1h
      retain_for: 24h
    correlation_checks:
      enabled: true
      interval: 6h
      retain_for: 7d
    tracks:
      enabled: true
      interval: 24h
      archive_after: 12months

websocket:
  enabled: true
  ping_interval: 30s
  max_connections_per_org: 100

  # Message buffering
  buffer_size: 256
  write_timeout: 10s

webhook:
  base_url: ${WEBHOOK_BASE_URL}
  path: /api/v1/webhooks/flightaware

  # Security
  verify_signature: true
  allowed_ips:
    - "52.0.0.0/8"
    - "54.0.0.0/8"

  # Retry configuration
  max_retries: 3
  retry_delay: 5s

alerts:
  # Severity thresholds
  severity_levels:
    critical:
      channels: [websocket, email, sms]
      sound: true
    high:
      channels: [websocket, email]
      sound: true
    medium:
      channels: [websocket]
      sound: false
    low:
      channels: [websocket]
      sound: false

  # Auto-acknowledge settings
  auto_acknowledge:
    enabled: false
    after: 24h

9. Testing Strategy

Unit Tests

// internal/adapters/tracking/flightaware_test.go

package tracking

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestFlightAwareAdapter_GetFlightPosition(t *testing.T) {
    // Mock server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Equal(t, "GET", r.Method)
        assert.Equal(t, "/aeroapi/flights/TEST123/position", r.URL.Path)
        assert.Equal(t, "test-api-key", r.Header.Get("x-apikey"))

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{
            "position": {
                "fa_flight_id": "TEST123",
                "callsign": "IBE123",
                "registration": "EC-ABC",
                "latitude": 41.385,
                "longitude": 2.173,
                "altitude": 35000,
                "groundspeed": 450,
                "heading": 90,
                "timestamp": 1705315200
            }
        }`))
    }))
    defer server.Close()

    adapter, err := NewFlightAwareAdapter(Config{
        APIKey:  "test-api-key",
        BaseURL: server.URL + "/aeroapi",
    })
    require.NoError(t, err)

    position, err := adapter.GetFlightPosition(context.Background(), "TEST123")
    require.NoError(t, err)

    assert.Equal(t, "TEST123", position.FAFlightID)
    assert.Equal(t, "IBE123", position.Callsign)
    assert.Equal(t, 41.385, position.Latitude)
    assert.Equal(t, 35000, position.Altitude)
}

func TestFlightAwareAdapter_VerifyWebhookSignature(t *testing.T) {
    adapter, err := NewFlightAwareAdapter(Config{
        APIKey:    "test-key",
        APISecret: "webhook-secret",
    })
    require.NoError(t, err)

    payload := []byte(`{"event": "test"}`)

    // Generate valid signature
    mac := hmac.New(sha256.New, []byte("webhook-secret"))
    mac.Write(payload)
    validSig := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    tests := []struct {
        name      string
        signature string
        wantErr   bool
    }{
        {"valid signature", validSig, false},
        {"invalid signature", "sha256=invalid", true},
        {"missing prefix", hex.EncodeToString(mac.Sum(nil)), true},
        {"empty signature", "", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := adapter.VerifyWebhookSignature(context.Background(), payload, tt.signature)
            if tt.wantErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }
        })
    }
}

Integration Tests

// tests/integration/webhook_test.go

package integration

import (
    "bytes"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/labstack/echo/v4"
    "github.com/stretchr/testify/suite"
)

type WebhookIntegrationTestSuite struct {
    suite.Suite
    server  *httptest.Server
    handler *handlers.WebhookHandler
}

func (s *WebhookIntegrationTestSuite) SetupSuite() {
    // Setup test database
    // Initialize handler with test dependencies
}

func (s *WebhookIntegrationTestSuite) TestFlightAwareWebhook_Departure() {
    e := echo.New()

    payload := []byte(`{
        "id": "evt_123",
        "type": "flight",
        "event_type": "departure",
        "timestamp": 1705315200,
        "fa_flight_id": "TEST123",
        "payload": {
            "actual_out": "2024-01-15T12:00:00Z"
        }
    }`)

    // Generate valid signature
    mac := hmac.New(sha256.New, []byte("test-secret"))
    mac.Write(payload)
    signature := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    req := httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/flightaware", bytes.NewReader(payload))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-FlightAware-Signature", signature)

    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)

    err := s.handler.HandleFlightAwareWebhook(c)
    s.NoError(err)
    s.Equal(http.StatusOK, rec.Code)

    // Verify database state
    // Verify WebSocket broadcast
    // Verify alert creation
}

Frontend Component Tests

// frontend/src/components/fleet/__tests__/FleetDashboard.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { FleetDashboard } from '../FleetDashboard';
import { server } from '../../../mocks/server';
import { rest } from 'msw';

describe('FleetDashboard', () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
    },
  });

  it('renders loading state initially', () => {
    render(
      <QueryClientProvider client={queryClient}>
        <FleetDashboard />
      </QueryClientProvider>
    );

    expect(screen.getByRole('progressbar')).toBeInTheDocument();
  });

  it('renders fleet data after loading', async () => {
    server.use(
      rest.get('/api/v1/fleet', (req, res, ctx) => {
        return res(ctx.json([
          {
            id: '1',
            registration: 'EC-ABC',
            type: 'Cessna Citation X',
            status: 'active',
            latitude: 41.385,
            longitude: 2.173,
          }
        ]));
      })
    );

    render(
      <QueryClientProvider client={queryClient}>
        <FleetDashboard />
      </QueryClientProvider>
    );

    await waitFor(() => {
      expect(screen.getByText('EC-ABC')).toBeInTheDocument();
    });
  });
});

Load Testing

// tests/load/poller_load_test.go

package load

import (
    "context"
    "sync"
    "testing"
    "time"
)

func BenchmarkPollingService_PollAllFlights(b *testing.B) {
    service := setupPollingService()

    // Add 100 active flights
    for i := 0; i < 100; i++ {
        service.AddFlight(uuid.New(), fmt.Sprintf("FLIGHT%d", i))
    }

    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        service.pollAllFlights(ctx)
        cancel()
    }
}

func TestPollingService_ConcurrentPolls(t *testing.T) {
    service := setupPollingService()

    var wg sync.WaitGroup
    errors := make(chan error, 100)

    // Simulate 100 concurrent polling requests
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            ctx := context.Background()
            _, err := service.trackingProvider.GetFlightPosition(ctx, fmt.Sprintf("TEST%d", id))
            if err != nil {
                errors <- err
            }
        }(i)
    }

    wg.Wait()
    close(errors)

    errCount := 0
    for err := range errors {
        if err != nil {
            errCount++
        }
    }

    // Should not exceed rate limit errors
    t.Logf("Errors: %d", errCount)
}

End-to-End Testing Checklist

Pre-flight: - [ ] Create test flight - [ ] Start tracking - [ ] Verify polling begins - [ ] Check initial position on map

During Flight: - [ ] Verify position updates every 60s - [ ] Check weather correlation every 5m - [ ] Test manual map interactions - [ ] Verify WebSocket updates

Webhook Events: - [ ] Simulate departure webhook - [ ] Simulate position update webhook - [ ] Simulate diversion webhook - [ ] Simulate arrival webhook - [ ] Verify HMAC signature validation - [ ] Test invalid signature rejection

Alert Flow: - [ ] Trigger weather alert - [ ] Receive WebSocket notification - [ ] View alert in UI - [ ] Acknowledge alert - [ ] Verify acknowledgment persists

Post-flight: - [ ] Verify track data saved - [ ] Download track in KML/GPX format - [ ] Check compliance report includes tracking data - [ ] Verify position data cleanup after 24h


10. Deployment Checklist

Pre-deployment

  • [ ] FlightAware Enterprise account provisioned
  • [ ] API key and webhook secret generated
  • [ ] Webhook endpoint configured in FlightAware dashboard
  • [ ] SSL certificate valid for webhook endpoint
  • [ ] Database migrations tested in staging
  • [ ] Environment variables configured
  • [ ] Redis instance provisioned (if using)

Database Migrations

# Run migrations
cd /var/www/skybrief
./migrate -path migrations -database "$DATABASE_URL" up

# Verify tables created
psql $DATABASE_URL -c "\dt"

# Create initial partitions
psql $DATABASE_URL -c "SELECT create_aircraft_positions_partition();"

Deployment Steps

# 1. Deploy new backend version
git pull origin main
go build -o skybrief cmd/api/main.go
sudo systemctl restart skybrief

# 2. Verify health check
curl https://api.skybrief.io/health

# 3. Deploy frontend
npm run build
rsync -avz dist/ /var/www/skybrief.io/

# 4. Verify webhook endpoint
curl -X POST https://api.skybrief.io/api/v1/webhooks/flightaware \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

# 5. Start polling service (if separate process)
sudo systemctl start skybrief-tracker

# 6. Monitor logs
journalctl -u skybrief -f

Post-deployment Verification

# Test API endpoints
curl -H "Authorization: Bearer $TOKEN" \
  https://api.skybrief.io/api/v1/fleet

# Test WebSocket connection
wscat -c "wss://api.skybrief.io/ws/fleet-updates?token=$TOKEN"

# Verify webhook is receiving events
# (Trigger a test flight in FlightAware dashboard)

# Check database performance
psql $DATABASE_URL -c "
  SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename))
  FROM pg_tables
  WHERE tablename LIKE 'aircraft_positions%'
  ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
"

Rollback Plan

# If critical issues detected:

# 1. Stop polling service
sudo systemctl stop skybrief-tracker

# 2. Revert to previous version
git checkout v1.x.x
go build -o skybrief cmd/api/main.go
sudo systemctl restart skybrief

# 3. If database changes need rollback:
./migrate -path migrations -database "$DATABASE_URL" down 1

# 4. Disable FlightAware webhooks temporarily
# (In FlightAware dashboard)

Monitoring Setup

# Add to Prometheus configuration
scrape_configs:
  - job_name: 'skybrief-tracking'
    static_configs:
      - targets: ['localhost:9090']
    metrics_path: /metrics

# Key metrics to alert on:
# - tracking_poll_errors_rate > 0.1
# - tracking_webhook_processing_time > 5s
# - tracking_active_flights (should be > 0 during business hours)
# - database_connection_pool_usage > 0.8


Document Version: 1.0
Last Updated: March 4, 2026
Next Review: After implementation completion

results matching ""

    No results matching ""