Make air-traffic refresh interval configurable, resolved from plan
Add a refresh-interval control to the Map settings popover: Auto (follows the OpenSky plan) or a fixed 15/30/60/120s. The states endpoint now resolves a recommended interval from the resolved plan (anonymous 60s, standard 30s, contributor 15s) and returns it alongside the aircraft, so "Auto" tracks the plan's credit budget and update rate. Polling reschedules live when the effective interval changes; the map caption reflects it. New airTrafficInterval preference (default 'auto', persisted + synced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2509fb9c07
commit
125d0ccc7d
@@ -564,8 +564,15 @@ func (s *Server) handleOpenSkyStates(w http.ResponseWriter, r *http.Request) {
|
||||
who := callerFromRecord(rec)
|
||||
res := s.resolveOpenSky(r.Context(), who, rec.Record["pluginSettings"])
|
||||
|
||||
// The plan drives the recommended refresh cadence (its daily credit budget and
|
||||
// upstream update rate). Surfaced on every response so the "Auto" interval in the
|
||||
// Map settings popover resolves even before the overlay is enabled.
|
||||
plan := res.eff.Plan
|
||||
recInterval := recommendedOpenSkyInterval(plan)
|
||||
disabled := func(detail string) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"states": []osAircraft{}, "unavailable": true, "detail": detail})
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"states": []osAircraft{}, "unavailable": true, "detail": detail,
|
||||
"plan": plan, "recommendedInterval": recInterval})
|
||||
}
|
||||
switch {
|
||||
case !res.available:
|
||||
@@ -630,7 +637,26 @@ func (s *Server) handleOpenSkyStates(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
aircraft = append(aircraft, a)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"time": osResp.Time, "states": aircraft})
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"time": osResp.Time, "states": aircraft,
|
||||
"plan": plan, "recommendedInterval": recInterval})
|
||||
}
|
||||
|
||||
// recommendedOpenSkyInterval maps a resolved plan to a sensible default poll
|
||||
// interval (seconds) for the Live map. It balances the plan's daily credit budget
|
||||
// (anonymous 400 · standard 4000 · contributor 8000) against how often OpenSky
|
||||
// actually refreshes state vectors (~10s anonymous, ~5s authenticated), so the map
|
||||
// stays fresh without draining credits. An unset plan resolves as standard (the
|
||||
// runtime default). See planDailyCredits in the opensky plugin.
|
||||
func recommendedOpenSkyInterval(plan string) int {
|
||||
switch plan {
|
||||
case "anonymous":
|
||||
return 60
|
||||
case "contributor":
|
||||
return 15
|
||||
default: // standard or unset
|
||||
return 30
|
||||
}
|
||||
}
|
||||
|
||||
// rawFloat reads element i of an OpenSky state array as a float, reporting ok=false
|
||||
|
||||
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -35,8 +35,8 @@
|
||||
})()
|
||||
</script>
|
||||
<title>PilotVault — Control Panel</title>
|
||||
<script type="module" crossorigin src="./assets/index-CYlCe7LJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Cy2Ed59q.css">
|
||||
<script type="module" crossorigin src="./assets/index-BdIdzNiz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CYeDXsw9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
+11
-2
@@ -168,13 +168,22 @@ export async function testOpenSky() {
|
||||
|
||||
// Live aircraft positions (OpenSky state vectors) within the caller's resolved
|
||||
// bounding box, for plotting on the Live map. Returns { states, unavailable?,
|
||||
// detail? } — an empty list with `unavailable` when OpenSky is off for the caller.
|
||||
// detail?, plan, recommendedInterval } — an empty list with `unavailable` when
|
||||
// OpenSky is off for the caller. `recommendedInterval` (seconds) is derived from
|
||||
// the resolved plan and drives the "Auto" refresh cadence.
|
||||
export async function getOpenSkyStates() {
|
||||
try {
|
||||
const r = await fetch('/bff/integrations/opensky/states')
|
||||
if (!r.ok) return { states: [], unavailable: true, detail: 'OpenSky unavailable' }
|
||||
const d = await r.json()
|
||||
return { states: d.states || [], time: d.time, unavailable: !!d.unavailable, detail: d.detail || '' }
|
||||
return {
|
||||
states: d.states || [],
|
||||
time: d.time,
|
||||
unavailable: !!d.unavailable,
|
||||
detail: d.detail || '',
|
||||
plan: d.plan || '',
|
||||
recommendedInterval: d.recommendedInterval || 0,
|
||||
}
|
||||
} catch {
|
||||
return { states: [], unavailable: true, detail: 'OpenSky unavailable' }
|
||||
}
|
||||
|
||||
@@ -27,29 +27,51 @@ const search = ref('')
|
||||
|
||||
/* ---------- OpenSky live air traffic (Overview map overlay) ---------- */
|
||||
const aircraft = ref([]) // [{ icao24, callsign, lat, lng, heading, velocity, altitude, onGround }]
|
||||
const airspace = reactive({ unavailable: false, detail: '', loaded: false })
|
||||
// plan + recommendedInterval are resolved server-side from the OpenSky plan.
|
||||
const airspace = reactive({ unavailable: false, detail: '', loaded: false, plan: '', recommendedInterval: 30 })
|
||||
const airborneCount = computed(() => aircraft.value.filter((a) => !a.onGround).length)
|
||||
const mapMenu = ref(false) // "Map settings" popover open state
|
||||
let airTimer = null
|
||||
|
||||
// User-selectable cadences; "Auto" defers to the plan-recommended interval.
|
||||
const INTERVAL_OPTIONS = [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 15, label: '15s' },
|
||||
{ value: 30, label: '30s' },
|
||||
{ value: 60, label: '60s' },
|
||||
{ value: 120, label: '120s' },
|
||||
]
|
||||
|
||||
// Effective poll interval in seconds: the plan recommendation when 'auto', else the
|
||||
// chosen fixed value. Falls back to 30s if the recommendation hasn't loaded yet.
|
||||
const airIntervalSeconds = computed(() => {
|
||||
if (prefs.airTrafficInterval === 'auto') return airspace.recommendedInterval || 30
|
||||
const n = Number(prefs.airTrafficInterval)
|
||||
return Number.isFinite(n) && n > 0 ? n : 30
|
||||
})
|
||||
|
||||
async function refreshAirspace() {
|
||||
if (!prefs.showAirTraffic) return
|
||||
const { states, unavailable, detail } = await getOpenSkyStates()
|
||||
const { states, unavailable, detail, plan, recommendedInterval } = await getOpenSkyStates()
|
||||
aircraft.value = states
|
||||
airspace.unavailable = unavailable
|
||||
airspace.detail = detail
|
||||
airspace.plan = plan || ''
|
||||
if (recommendedInterval) airspace.recommendedInterval = recommendedInterval
|
||||
airspace.loaded = true
|
||||
}
|
||||
|
||||
// Poll OpenSky only while the Overview map is on screen and the overlay is
|
||||
// enabled, on a credit-friendly cadence (state vectors refresh at most every
|
||||
// ~10s upstream anyway).
|
||||
function startAirspace() {
|
||||
if (airTimer) return
|
||||
refreshAirspace()
|
||||
// Poll OpenSky only while the Overview map is on screen and the overlay is enabled,
|
||||
// on the resolved cadence (state vectors refresh at most every ~5–10s upstream).
|
||||
function scheduleAirspace() {
|
||||
if (airTimer) clearInterval(airTimer)
|
||||
airTimer = setInterval(() => {
|
||||
if (active.value === 'Overview' && prefs.showAirTraffic) refreshAirspace()
|
||||
}, 30000)
|
||||
}, airIntervalSeconds.value * 1000)
|
||||
}
|
||||
function startAirspace() {
|
||||
refreshAirspace()
|
||||
scheduleAirspace()
|
||||
}
|
||||
function stopAirspace() {
|
||||
if (airTimer) clearInterval(airTimer)
|
||||
@@ -296,6 +318,10 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// Reschedule polling whenever the effective interval changes (a manual choice, or
|
||||
// the plan recommendation arriving while on "Auto").
|
||||
watch(airIntervalSeconds, scheduleAirspace)
|
||||
|
||||
onMounted(async () => {
|
||||
;(await getDevices()).forEach(upsert)
|
||||
connect()
|
||||
@@ -450,12 +476,26 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<template v-if="mapMenu">
|
||||
<div class="fixed inset-0 z-[1190]" @click="mapMenu = false"></div>
|
||||
<div class="panel absolute right-0 z-[1200] mt-1.5 w-64 p-3.5 shadow-lg">
|
||||
<div class="panel absolute right-0 z-[1200] mt-1.5 w-72 p-3.5 shadow-lg">
|
||||
<div class="eyebrow mb-2.5">Map settings</div>
|
||||
<label class="flex items-center justify-between gap-3">
|
||||
<span class="text-sm text-ink-secondary">Show live air traffic</span>
|
||||
<Toggle v-model="prefs.showAirTraffic" />
|
||||
</label>
|
||||
<div class="mt-3.5" :class="prefs.showAirTraffic ? '' : 'pointer-events-none opacity-40'">
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<span class="text-sm text-ink-secondary">Refresh interval</span>
|
||||
<span class="font-mono text-[11px] text-ink-muted">every {{ airIntervalSeconds }}s</span>
|
||||
</div>
|
||||
<select v-model="prefs.airTrafficInterval" class="field">
|
||||
<option v-for="o in INTERVAL_OPTIONS" :key="o.value" :value="o.value">
|
||||
{{ o.label }}{{ o.value === 'auto' ? ` (plan: ${airspace.recommendedInterval}s)` : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="airspace.plan" class="mt-1.5 text-[11px] text-ink-muted">
|
||||
OpenSky plan: {{ airspace.plan }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -469,7 +509,7 @@ onBeforeUnmount(() => {
|
||||
{{ airspace.detail || 'Live air traffic is unavailable.' }}
|
||||
</p>
|
||||
<p v-else class="mt-2.5 text-xs text-ink-muted">
|
||||
Live air traffic from OpenSky Network · updates every 30s
|
||||
Live air traffic from OpenSky Network · updates every {{ airIntervalSeconds }}s
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ const defaults = {
|
||||
// Live map: overlay live OpenSky air traffic (client-side toggle; the OpenSky
|
||||
// integration itself is still gated in Settings → Integrations).
|
||||
showAirTraffic: true,
|
||||
// Air-traffic refresh cadence: 'auto' follows the plan-recommended interval
|
||||
// (from the server), or a fixed number of seconds (15 | 30 | 60 | 120).
|
||||
airTrafficInterval: 'auto',
|
||||
// Security (prototype)
|
||||
twoFactor: false,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user