Aurora update
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { AirTrafficLayer } from '@/services/airtraffic.ts'
|
||||
import {api} from '@/services/api.ts';
|
||||
import {AuroraLayer} from '@/services/aurora.ts';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import Map from 'ol/Map'
|
||||
import View from 'ol/View'
|
||||
@@ -191,6 +192,9 @@ onMounted(async () => {
|
||||
controls: [],
|
||||
})
|
||||
|
||||
const aurora = new AuroraLayer(map)
|
||||
aurora.show();
|
||||
|
||||
airTraffic = new AirTrafficLayer(map)
|
||||
airTraffic.show()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {formatDate} from '@ztimson/utils';
|
||||
import {ref, onMounted, computed, watchEffect} from 'vue';
|
||||
import {api} from '../services/api';
|
||||
import MetricRow from './MetricRow.vue';
|
||||
@@ -92,7 +93,7 @@ watchEffect(() => {
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
<MetricRow label="Next Full Moon" :value="new Date(d.moon_full).toLocaleDateString()" />
|
||||
<MetricRow label="Next New Moon" :value="new Date(d.moon_new).toLocaleDateString()" />
|
||||
<MetricRow label="Next Full Moon" :value="formatDate('MMM D', d.moon_full)" />
|
||||
<MetricRow label="Next New Moon" :value="formatDate('MMM D', d.moon_new)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
98
server/src/aurora.mjs
Normal file
98
server/src/aurora.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import {adjustedInterval, Logger} from '@ztimson/utils';
|
||||
|
||||
const cacheOptions = {
|
||||
ttl: null,
|
||||
reload: null,
|
||||
retry: 3,
|
||||
url: null,
|
||||
}
|
||||
|
||||
export class CacheService {
|
||||
#fetchLoop;
|
||||
|
||||
cached = null;
|
||||
lastUpdate = null;
|
||||
logger;
|
||||
name;
|
||||
options;
|
||||
pending;
|
||||
|
||||
constructor(name, options) {
|
||||
this.name = name;
|
||||
this.options = {
|
||||
...cacheOptions,
|
||||
ttl: options.reload,
|
||||
...options
|
||||
};
|
||||
this.logger = new Logger(name);
|
||||
if(this.options.reload) setTimeout(() => this.startLoop(), 1000);
|
||||
}
|
||||
|
||||
async #fetchWithRetry() {
|
||||
let lastError;
|
||||
for(let attempt = 1; attempt <= this.options.retry; attempt++) {
|
||||
try {
|
||||
return await this.fetch();
|
||||
} catch(err) {
|
||||
lastError = err;
|
||||
if(attempt < this.options.retry)
|
||||
this.logger.warn(`Retrying (${attempt + 1}/${this.options.retry}): ${err.message || err.toString()}`);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
if(!this.options.url) return Promise.reject('Fetch is not configured');
|
||||
const resp = await fetch(this.options.url);
|
||||
if(!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
startLoop(speed = this.options.reload) {
|
||||
this.stopLoop();
|
||||
this.#fetchLoop = adjustedInterval(() => this.get(), speed);
|
||||
}
|
||||
|
||||
stopLoop() {
|
||||
if(!this.#fetchLoop) return;
|
||||
clearInterval(this.#fetchLoop);
|
||||
this.#fetchLoop = null;
|
||||
}
|
||||
|
||||
update(catchErr = true) {
|
||||
if(this.pending) return this.pending;
|
||||
this.logger.info('Fetching latest');
|
||||
this.pending = this.#fetchWithRetry().then(data => {
|
||||
if(data?.err) throw new Error(data.err?.stack || data.err?.message || data.err);
|
||||
if(data?.error) throw new Error(data.error?.stack || data.error?.message || data.error);
|
||||
this.lastUpdate = new Date();
|
||||
this.cached = data || null;
|
||||
this.logger.debug('Finished updating');
|
||||
return {timestamp: this.lastUpdate, data: this.cached};
|
||||
}).catch(err => {
|
||||
if(catchErr) this.logger.error(`Failed: ${typeof err == 'object' ? (err.stackTrace || err.message) : err}`)
|
||||
else throw err;
|
||||
}).finally(() => this.pending = null);
|
||||
return this.pending;
|
||||
}
|
||||
|
||||
async get(wait = false) {
|
||||
if(!this.cached || !this.lastUpdate || this.lastUpdate?.getTime() + this.options.ttl <= Date.now()) {
|
||||
if(wait) await this.update();
|
||||
else this.update();
|
||||
}
|
||||
return Promise.resolve({timestamp: this.lastUpdate || null, data: this.cached || null});
|
||||
}
|
||||
}
|
||||
|
||||
export class AuroraService extends CacheService {
|
||||
constructor() {
|
||||
super('Aurora', {
|
||||
reload: 60_000 * 15,
|
||||
url: 'https://services.swpc.noaa.gov/json/ovation_aurora_latest.json',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const Aurora = new AuroraService();
|
||||
@@ -12,6 +12,7 @@ import {getAirTraffic, getAirTrafficHistory, getShapes} from './airtraffic.mjs';
|
||||
import {getWeatherCondition} from './openweather.mjs';
|
||||
import {lastForecast, getForecast, forecastTTL} from './forecast.mjs';
|
||||
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
|
||||
import {Aurora} from './aurora.mjs';
|
||||
|
||||
// ── Uncaught error handlers ───────────────────────────────────────────────────
|
||||
|
||||
@@ -145,6 +146,7 @@ app.get('/api/position', async (req, res) => {
|
||||
|
||||
// ── Space ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/aurora', async (req, res) => res.json(await Aurora.get()));
|
||||
app.get('/api/space', async (req, res) => {
|
||||
const {fields} = req.query;
|
||||
res.json(filterFields(await getSpaceWeather(), fields));
|
||||
|
||||
Reference in New Issue
Block a user