diff --git a/API Server/internal/api/integrations.go b/API Server/internal/api/integrations.go
index 8c90859..38172cc 100644
--- a/API Server/internal/api/integrations.go
+++ b/API Server/internal/api/integrations.go
@@ -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
diff --git a/Web App/server/dist/assets/index-BdIdzNiz.js b/Web App/server/dist/assets/index-BdIdzNiz.js
new file mode 100644
index 0000000..d0b234c
--- /dev/null
+++ b/Web App/server/dist/assets/index-BdIdzNiz.js
@@ -0,0 +1,20 @@
+(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const u of document.querySelectorAll('link[rel="modulepreload"]'))l(u);new MutationObserver(u=>{for(const d of u)if(d.type==="childList")for(const h of d.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&l(h)}).observe(document,{childList:!0,subtree:!0});function o(u){const d={};return u.integrity&&(d.integrity=u.integrity),u.referrerPolicy&&(d.referrerPolicy=u.referrerPolicy),u.crossOrigin==="use-credentials"?d.credentials="include":u.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function l(u){if(u.ep)return;u.ep=!0;const d=o(u);fetch(u.href,d)}})();/**
+* @vue/shared v3.5.39
+* (c) 2018-present Yuxi (Evan) You and Vue contributors
+* @license MIT
+**/function Pr(t){const i=Object.create(null);for(const o of t.split(","))i[o]=1;return o=>o in i}const pt={},xs=[],Un=()=>{},mu=()=>!1,pa=t=>t.charCodeAt(0)===111&&t.charCodeAt(1)===110&&(t.charCodeAt(2)>122||t.charCodeAt(2)<97),ma=t=>t.startsWith("onUpdate:"),At=Object.assign,Cr=(t,i)=>{const o=t.indexOf(i);o>-1&&t.splice(o,1)},cd=Object.prototype.hasOwnProperty,ct=(t,i)=>cd.call(t,i),De=Array.isArray,ws=t=>yo(t)==="[object Map]",Es=t=>yo(t)==="[object Set]",hl=t=>yo(t)==="[object Date]",je=t=>typeof t=="function",bt=t=>typeof t=="string",Ln=t=>typeof t=="symbol",dt=t=>t!==null&&typeof t=="object",gu=t=>(dt(t)||je(t))&&je(t.then)&&je(t.catch),vu=Object.prototype.toString,yo=t=>vu.call(t),dd=t=>yo(t).slice(8,-1),_u=t=>yo(t)==="[object Object]",Lr=t=>bt(t)&&t!=="NaN"&&t[0]!=="-"&&""+parseInt(t,10)===t,io=Pr(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ga=t=>{const i=Object.create(null);return(o=>i[o]||(i[o]=t(o)))},fd=/-\w/g,Pn=ga(t=>t.replace(fd,i=>i.slice(1).toUpperCase())),hd=/\B([A-Z])/g,Ei=ga(t=>t.replace(hd,"-$1").toLowerCase()),yu=ga(t=>t.charAt(0).toUpperCase()+t.slice(1)),Ya=ga(t=>t?`on${yu(t)}`:""),Vn=(t,i)=>!Object.is(t,i),ea=(t,...i)=>{for(let o=0;othis.options.maxZoom)?this.setZoom(e):this},panInsideBounds:function(e,n){this._enforcingBounds=!0;var s=this.getCenter(),a=this._limitCenter(s,this._zoom,et(e));return s.equals(a)||this.panTo(a,n),this._enforcingBounds=!1,this},panInside:function(e,n){n=n||{};var s=me(n.paddingTopLeft||n.padding||[0,0]),a=me(n.paddingBottomRight||n.padding||[0,0]),c=this.project(this.getCenter()),m=this.project(e),C=this.getPixelBounds(),$=Je([C.min.add(s),C.max.subtract(a)]),N=$.getSize();if(!$.contains(m)){this._enforcingBounds=!0;var J=m.subtract($.getCenter()),ge=$.extend(m).getSize().subtract(N);c.x+=J.x<0?-ge.x:ge.x,c.y+=J.y<0?-ge.y:ge.y,this.panTo(this.unproject(c),n),this._enforcingBounds=!1}return this},invalidateSize:function(e){if(!this._loaded)return this;e=u({animate:!1,pan:!0},e===!0?{animate:!0}:e);var n=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var s=this.getSize(),a=n.divideBy(2).round(),c=s.divideBy(2).round(),m=a.subtract(c);return!m.x&&!m.y?this:(e.animate&&e.pan?this.panBy(m):(e.pan&&this._rawPanBy(m),this.fire("move"),e.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(h(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:n,newSize:s}))},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(e){if(e=this._locateOptions=u({timeout:1e4,watch:!1},e),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var n=h(this._handleGeolocationResponse,this),s=h(this._handleGeolocationError,this);return e.watch?this._locationWatchId=navigator.geolocation.watchPosition(n,s,e):navigator.geolocation.getCurrentPosition(n,s,e),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(e){if(this._container._leaflet_id){var n=e.code,s=e.message||(n===1?"permission denied":n===2?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:n,message:"Geolocation error: "+s+"."})}},_handleGeolocationResponse:function(e){if(this._container._leaflet_id){var n=e.coords.latitude,s=e.coords.longitude,a=new Ze(n,s),c=a.toBounds(e.coords.accuracy*2),m=this._locateOptions;if(m.setView){var C=this.getBoundsZoom(c);this.setView(a,m.maxZoom?Math.min(C,m.maxZoom):C)}var $={latlng:a,bounds:c,timestamp:e.timestamp};for(var N in e.coords)typeof e.coords[N]=="number"&&($[N]=e.coords[N]);this.fire("locationfound",$)}},addHandler:function(e,n){if(!n)return this;var s=this[e]=new n(this);return this._handlers.push(s),this.options[e]&&s.enable(),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch{this._container._leaflet_id=void 0,this._containerId=void 0}this._locationWatchId!==void 0&&this.stopLocate(),this._stop(),lt(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(te(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload");var e;for(e in this._layers)this._layers[e].remove();for(e in this._panes)lt(this._panes[e]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(e,n){var s="leaflet-pane"+(e?" leaflet-"+e.replace("Pane","")+"-pane":""),a=oe("div",s,n||this._mapPane);return e&&(this._panes[e]=a),a},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var e=this.getPixelBounds(),n=this.unproject(e.getBottomLeft()),s=this.unproject(e.getTopRight());return new at(n,s)},getMinZoom:function(){return this.options.minZoom===void 0?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return this.options.maxZoom===void 0?this._layersMaxZoom===void 0?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(e,n,s){e=et(e),s=me(s||[0,0]);var a=this.getZoom()||0,c=this.getMinZoom(),m=this.getMaxZoom(),C=e.getNorthWest(),$=e.getSouthEast(),N=this.getSize().subtract(s),J=Je(this.project($,a),this.project(C,a)).getSize(),ge=we.any3d?this.options.zoomSnap:1,Be=N.x/J.x,qe=N.y/J.y,Kt=n?Math.max(Be,qe):Math.min(Be,qe);return a=this.getScaleZoom(Kt,a),ge&&(a=Math.round(a/(ge/100))*(ge/100),a=n?Math.ceil(a/ge)*ge:Math.floor(a/ge)*ge),Math.max(c,Math.min(m,a))},getSize:function(){return(!this._size||this._sizeChanged)&&(this._size=new ie(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(e,n){var s=this._getTopLeftPoint(e,n);return new ze(s,s.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(e){return this.options.crs.getProjectedBounds(e===void 0?this.getZoom():e)},getPane:function(e){return typeof e=="string"?this._panes[e]:e},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(e,n){var s=this.options.crs;return n=n===void 0?this._zoom:n,s.scale(e)/s.scale(n)},getScaleZoom:function(e,n){var s=this.options.crs;n=n===void 0?this._zoom:n;var a=s.zoom(e*s.scale(n));return isNaN(a)?1/0:a},project:function(e,n){return n=n===void 0?this._zoom:n,this.options.crs.latLngToPoint(K(e),n)},unproject:function(e,n){return n=n===void 0?this._zoom:n,this.options.crs.pointToLatLng(me(e),n)},layerPointToLatLng:function(e){var n=me(e).add(this.getPixelOrigin());return this.unproject(n)},latLngToLayerPoint:function(e){var n=this.project(K(e))._round();return n._subtract(this.getPixelOrigin())},wrapLatLng:function(e){return this.options.crs.wrapLatLng(K(e))},wrapLatLngBounds:function(e){return this.options.crs.wrapLatLngBounds(et(e))},distance:function(e,n){return this.options.crs.distance(K(e),K(n))},containerPointToLayerPoint:function(e){return me(e).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(e){return me(e).add(this._getMapPanePos())},containerPointToLatLng:function(e){var n=this.containerPointToLayerPoint(me(e));return this.layerPointToLatLng(n)},latLngToContainerPoint:function(e){return this.layerPointToContainerPoint(this.latLngToLayerPoint(K(e)))},mouseEventToContainerPoint:function(e){return Eo(e,this._container)},mouseEventToLayerPoint:function(e){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e))},mouseEventToLatLng:function(e){return this.layerPointToLatLng(this.mouseEventToLayerPoint(e))},_initContainer:function(e){var n=this._container=Co(e);if(n){if(n._leaflet_id)throw new Error("Map container is already initialized.")}else throw new Error("Map container not found.");He(n,"scroll",this._onScroll,this),this._containerId=y(n)},_initLayout:function(){var e=this._container;this._fadeAnimated=this.options.fadeAnimation&&we.any3d,Fe(e,"leaflet-container"+(we.touch?" leaflet-touch":"")+(we.retina?" leaflet-retina":"")+(we.ielt9?" leaflet-oldie":"")+(we.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var n=zi(e,"position");n!=="absolute"&&n!=="relative"&&n!=="fixed"&&n!=="sticky"&&(e.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var e=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),_t(this._mapPane,new ie(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(Fe(e.markerPane,"leaflet-zoom-hide"),Fe(e.shadowPane,"leaflet-zoom-hide"))},_resetView:function(e,n,s){_t(this._mapPane,new ie(0,0));var a=!this._loaded;this._loaded=!0,n=this._limitZoom(n),this.fire("viewprereset");var c=this._zoom!==n;this._moveStart(c,s)._move(e,n)._moveEnd(c),this.fire("viewreset"),a&&this.fire("load")},_moveStart:function(e,n){return e&&this.fire("zoomstart"),n||this.fire("movestart"),this},_move:function(e,n,s,a){n===void 0&&(n=this._zoom);var c=this._zoom!==n;return this._zoom=n,this._lastCenter=e,this._pixelOrigin=this._getNewPixelOrigin(e),a?s&&s.pinch&&this.fire("zoom",s):((c||s&&s.pinch)&&this.fire("zoom",s),this.fire("move",s)),this},_moveEnd:function(e){return e&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return te(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(e){_t(this._mapPane,this._getMapPanePos().subtract(e))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(e){this._targets={},this._targets[y(this._container)]=this;var n=e?ut:He;n(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&n(window,"resize",this._onResize,this),we.any3d&&this.options.transform3DLimit&&(e?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){te(this._resizeRequest),this._resizeRequest=Te(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var e=this._getMapPanePos();Math.max(Math.abs(e.x),Math.abs(e.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(e,n){for(var s=[],a,c=n==="mouseout"||n==="mouseover",m=e.target||e.srcElement,C=!1;m;){if(a=this._targets[y(m)],a&&(n==="click"||n==="preclick")&&this._draggableMoved(a)){C=!0;break}if(a&&a.listens(n,!0)&&(c&&!Kn(m,e)||(s.push(a),c))||m===this._container)break;m=m.parentNode}return!s.length&&!C&&!c&&this.listens(n,!0)&&(s=[this]),s},_isClickDisabled:function(e){for(;e&&e!==this._container;){if(e._leaflet_disable_click)return!0;e=e.parentNode}},_handleDOMEvent:function(e){var n=e.target||e.srcElement;if(!(!this._loaded||n._leaflet_disable_events||e.type==="click"&&this._isClickDisabled(n))){var s=e.type;s==="mousedown"&&os(n),this._fireDOMEvent(e,s)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(e,n,s){if(e.type==="click"){var a=u({},e);a.type="preclick",this._fireDOMEvent(a,a.type,s)}var c=this._findEventTargets(e,n);if(s){for(var m=[],C=0;Cthis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),this.options.minZoom===void 0&&this._layersMinZoom&&this.getZoom()
- Live air traffic from OpenSky Network · updates every 30s + Live air traffic from OpenSky Network · updates every {{ airIntervalSeconds }}s
diff --git a/Web App/web/src/prefs.js b/Web App/web/src/prefs.js index a57c0c9..4355e96 100644 --- a/Web App/web/src/prefs.js +++ b/Web App/web/src/prefs.js @@ -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, }