Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 55 additions & 8 deletions core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (
)

type APIServer struct {
pg *postgres
port string
settings *SettingsService
pg *postgres
port string
settings *SettingsService
cachedStats *CachedStatsService
}

func NewAPIServer(pg *postgres) *APIServer {
Expand All @@ -25,9 +26,10 @@ func NewAPIServer(pg *postgres) *APIServer {
port = "8080"
}
return &APIServer{
pg: pg,
port: port,
settings: NewSettingsService(pg),
pg: pg,
port: port,
settings: NewSettingsService(pg),
cachedStats: NewCachedStatsService(pg),
}
}

Expand Down Expand Up @@ -79,8 +81,8 @@ func (s *APIServer) Start() {
{
stats.GET("/above", s.getAboveStats)

stats.GET("/seen/flights", s.getFlightsSeenMetrics)
stats.GET("/seen/aircraft", s.getAircraftSeenMetrics)
stats.GET("/seen/recent", s.getRecentSeenMetrics)
stats.GET("/seen/totals", s.getTotalSeenMetrics)

stats.GET("/routes/metrics", s.getRouteMetrics)
stats.GET("/routes/airlines", s.getTopAirlines)
Expand Down Expand Up @@ -119,6 +121,9 @@ func (s *APIServer) Start() {
stats.GET("/charts/aircraft/month", func(c *gin.Context) { s.getChartAircraftOverTime(c, "month") })
stats.GET("/charts/aircraft/day", func(c *gin.Context) { s.getChartAircraftOverTime(c, "day") })

// Depracacted
stats.GET("/seen/flights", s.getFlightsSeenMetrics)
stats.GET("/seen/aircraft", s.getAircraftSeenMetrics)
}

settings := api.Group("/settings")
Expand Down Expand Up @@ -1461,3 +1466,45 @@ func (s *APIServer) updateSettings(c *gin.Context) {
}
c.JSON(http.StatusOK, settings)
}

func (s *APIServer) getRecentSeenMetrics(c *gin.Context) {
stats := gin.H{}
tz := s.getTimezone(c)

query := `
SELECT
COUNT(*) FILTER (WHERE DATE(first_seen AT TIME ZONE $1) = CURRENT_DATE) as today_flights,
COUNT(*) FILTER (WHERE first_seen >= NOW() - INTERVAL '1 hour') as hour_flights,
COUNT(DISTINCT CASE WHEN DATE(first_seen AT TIME ZONE $1) = CURRENT_DATE THEN hex END) as today_aircraft,
COUNT(DISTINCT CASE WHEN first_seen >= NOW() - INTERVAL '1 hour' THEN hex END) as hour_aircraft
FROM aircraft_data
WHERE first_seen >= CURRENT_DATE AT TIME ZONE $1 - INTERVAL '1 day'
`

var todayFlights, hourFlights, todayAircraft, hourAircraft int
err := s.pg.db.QueryRow(context.Background(), query, tz).Scan(&todayFlights, &hourFlights, &todayAircraft, &hourAircraft)
if err == nil {
stats["today_flights"] = todayFlights
stats["hour_flights"] = hourFlights
stats["today_aircraft"] = todayAircraft
stats["hour_aircraft"] = hourAircraft
}

c.JSON(http.StatusOK, stats)
}

func (s *APIServer) getTotalSeenMetrics(c *gin.Context) {
stats := gin.H{}

totalFlights, err := s.cachedStats.GetCachedStat("total_flights")
if err == nil {
stats["total_flights"] = totalFlights
}

totalAircraft, err := s.cachedStats.GetCachedStat("total_aircraft")
if err == nil {
stats["total_aircraft"] = totalAircraft
}

c.JSON(http.StatusOK, stats)
}
90 changes: 90 additions & 0 deletions core/cached-stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"context"
"time"

"github.com/rs/zerolog/log"
)

type CachedStatsService struct {
pg *postgres
}

func NewCachedStatsService(pg *postgres) *CachedStatsService {
return &CachedStatsService{pg: pg}
}

func (s *CachedStatsService) GetCachedStat(key string) (int, error) {
var value int
err := s.pg.db.QueryRow(context.Background(),
"SELECT stat_value FROM cached_stats WHERE stat_key = $1", key).Scan(&value)
return value, err
}

func (s *CachedStatsService) UpdateCachedStat(key string, value int) error {
_, err := s.pg.db.Exec(context.Background(),
`INSERT INTO cached_stats (stat_key, stat_value, last_updated)
VALUES ($1, $2, NOW())
ON CONFLICT (stat_key)
DO UPDATE SET stat_value = $2, last_updated = NOW()`,
key, value)
return err
}

func (s *CachedStatsService) RefreshAllTotals() error {
log.Info().Msg("Refreshing cached all-time statistics...")

// Update total flights
var totalFlights int
err := s.pg.db.QueryRow(context.Background(),
"SELECT COUNT(*) FROM aircraft_data").Scan(&totalFlights)
if err != nil {
log.Error().Err(err).Msg("Failed to count total flights")
return err
}
err = s.UpdateCachedStat("total_flights", totalFlights)
if err != nil {
log.Error().Err(err).Msg("Failed to update cached total_flights")
return err
}

// Update total aircraft
var totalAircraft int
err = s.pg.db.QueryRow(context.Background(),
"SELECT COUNT(DISTINCT hex) FROM aircraft_data").Scan(&totalAircraft)
if err != nil {
log.Error().Err(err).Msg("Failed to count total aircraft")
return err
}
err = s.UpdateCachedStat("total_aircraft", totalAircraft)
if err != nil {
log.Error().Err(err).Msg("Failed to update cached total_aircraft")
return err
}

log.Info().
Int("total_flights", totalFlights).
Int("total_aircraft", totalAircraft).
Msg("Successfully refreshed cached statistics")

return nil
}

func (s *CachedStatsService) StartPeriodicRefresh(interval time.Duration) {
go func() {
// Initial refresh on startup
if err := s.RefreshAllTotals(); err != nil {
log.Error().Err(err).Msg("Initial cache refresh failed")
}

ticker := time.NewTicker(interval)
defer ticker.Stop()

for range ticker.C {
if err := s.RefreshAllTotals(); err != nil {
log.Error().Err(err).Msg("Periodic cache refresh failed")
}
}
}()
}
6 changes: 5 additions & 1 deletion core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,14 @@ func main() {
os.Exit(1)
}

apiServer := NewAPIServer(pg)

log.Info().Msg("Starting cached statistics refresh service")
apiServer.cachedStats.StartPeriodicRefresh(24 * time.Hour)

// Start API server in a separate goroutine
log.Info().Msg("Starting API server")
go func() {
apiServer := NewAPIServer(pg)
apiServer.Start()
}()

Expand Down
1 change: 1 addition & 0 deletions migrations/000007_add_cached_stats.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS cached_stats;
12 changes: 12 additions & 0 deletions migrations/000007_add_cached_stats.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Create table for cached statistics
CREATE TABLE cached_stats (
stat_key VARCHAR(50) PRIMARY KEY,
stat_value BIGINT NOT NULL,
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Insert initial values for all-time totals
INSERT INTO cached_stats (stat_key, stat_value, last_updated)
VALUES
('total_flights', (SELECT COUNT(*) FROM aircraft_data), NOW()),
('total_aircraft', (SELECT COUNT(DISTINCT hex) FROM aircraft_data), NOW());
1 change: 1 addition & 0 deletions migrations/000008_add_above_index.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP INDEX IF EXISTS idx_aircraft_data_last_seen_distance;
2 changes: 2 additions & 0 deletions migrations/000008_add_above_index.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE INDEX idx_aircraft_data_last_seen_distance
ON aircraft_data (last_seen DESC, last_seen_distance);
11 changes: 1 addition & 10 deletions web/src/components/InterestingAircraft.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
<script>
import { onMount, onDestroy } from 'svelte'
import { onMount } from 'svelte'
import { settings, refreshInterestingData } from '../stores/settings';

export let endpoint;
export let title;
export let icon;
export let aircraftType;

let refreshRate = 10000
let data = [];
let loading = true;
let error = null;
let interval = null;
let selectedAircraft = null;
let imageLoadingStates = {
image1: true,
Expand Down Expand Up @@ -53,15 +51,8 @@

onMount(() => {
fetchData();
interval = setInterval(fetchData, refreshRate)
})

onDestroy(() => {
if (interval) {
clearInterval(interval)
}
});

// Refresh when settings change
$: if ($refreshInterestingData) {
fetchData();
Expand Down
33 changes: 23 additions & 10 deletions web/src/components/MetricAircraftSeen.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@
import NumberFlow from '@number-flow/svelte'
import SkeletonMetrics from './SkeletonMetrics.svelte';


let data = {};
let endpoint = 'api/stats/seen/aircraft'
let endpoint = 'api/stats/seen/recent'
let loading = true;
let error = null;
let interval = null;

async function fetchData() {
let recentInterval = null;

async function fetchRecentData() {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const response = await fetch(`${endpoint}?tz=${encodeURIComponent(tz)}`);
if (!response.ok) {
throw new Error(`${response.status}`);
}
const result = await response.json();
data = result;
data = { ...data, ...result };
error = null;
} catch (err) {
error = err.message;
Expand All @@ -29,14 +27,29 @@
}
}

async function fetchTotalData() {
try {
const response = await fetch(`api/stats/seen/totals`);
if (!response.ok) {
throw new Error(`${response.status}`);
}
const result = await response.json();
data = { ...data, ...result };
error = null;
} catch (err) {
error = err.message;
}
}

onMount(() => {
fetchData();
interval = setInterval(fetchData, 2000);
fetchRecentData();
fetchTotalData();
recentInterval = setInterval(fetchRecentData, 2000);
})

onDestroy(() => {
if (interval) {
clearInterval(interval);
if (recentInterval) {
clearInterval(recentInterval);
}
});
</script>
Expand Down
32 changes: 23 additions & 9 deletions web/src/components/MetricFlightsSeen.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@
import SkeletonMetrics from './SkeletonMetrics.svelte';

let data = {};
let endpoint = 'api/stats/seen/flights'
let endpoint = 'api/stats/seen/recent'
let loading = true;
let error = null;
let interval = null;

async function fetchData() {
let recentInterval = null;

async function fetchRecentData() {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const response = await fetch(`${endpoint}?tz=${encodeURIComponent(tz)}`);
if (!response.ok) {
throw new Error(`${response.status}`);
}
const result = await response.json();
data = result;
data = { ...data, ...result };
error = null;
} catch (err) {
error = err.message;
Expand All @@ -28,14 +27,29 @@
}
}

async function fetchTotalData() {
try {
const response = await fetch(`api/stats/seen/totals`);
if (!response.ok) {
throw new Error(`${response.status}`);
}
const result = await response.json();
data = { ...data, ...result };
error = null;
} catch (err) {
error = err.message;
}
}

onMount(() => {
fetchData();
interval = setInterval(fetchData, 2000);
fetchRecentData();
fetchTotalData();
recentInterval = setInterval(fetchRecentData, 2000);
})

onDestroy(() => {
if (interval) {
clearInterval(interval);
if (recentInterval) {
clearInterval(recentInterval);
}
});
</script>
Expand Down
Loading
Loading