Forecast fixes
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Loading from '@/components/Loading.vue';
|
import Loading from '@/components/Loading.vue';
|
||||||
import {formatDate} from '@ztimson/utils';
|
import {formatDate} from '@ztimson/utils';
|
||||||
import {BASE, type DataRow} from '../services/api';
|
import {onMounted, ref} from 'vue';
|
||||||
|
import {api, BASE} from '../services/api';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ days: DataRow[] }>(), {});
|
const days = ref<any[]>()
|
||||||
const filler = [1, 2, 3, 4, 5];
|
const filler = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
function dir(deg: number) {
|
function dir(deg: number) {
|
||||||
@@ -16,6 +17,13 @@ function dir(deg: number) {
|
|||||||
if(deg > 292.5 && deg <= 337.5) return 'NW';
|
if(deg > 292.5 && deg <= 337.5) return 'NW';
|
||||||
return 'N';
|
return 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const start = new Date(), end = new Date();
|
||||||
|
start.setDate(start.getDate() + 1);
|
||||||
|
end.setDate(end.getDate() + 5);
|
||||||
|
days.value = await api.daily(start, end);
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import Point from 'ol/geom/Point'
|
|||||||
import CircleGeom from 'ol/geom/Circle'
|
import CircleGeom from 'ol/geom/Circle'
|
||||||
import { fromLonLat, toLonLat } from 'ol/proj'
|
import { fromLonLat, toLonLat } from 'ol/proj'
|
||||||
import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style'
|
import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style'
|
||||||
import { current } from '../services/weather'
|
|
||||||
import 'ol/ol.css'
|
import 'ol/ol.css'
|
||||||
|
|
||||||
|
const current = ref([0, 0]);
|
||||||
const props = defineProps<{ dark: boolean }>()
|
const props = defineProps<{ dark: boolean }>()
|
||||||
const mapEl = ref<HTMLDivElement>()
|
const mapEl = ref<HTMLDivElement>()
|
||||||
const showOverlays = ref(false)
|
const showOverlays = ref(false)
|
||||||
|
|||||||
@@ -16,5 +16,9 @@ export const api = {
|
|||||||
position: (fields?: string) => get<{latitude: number, longitude: number, altitude: number}>('/api/position', fields ? { fields } : {}),
|
position: (fields?: string) => get<{latitude: number, longitude: number, altitude: number}>('/api/position', fields ? { fields } : {}),
|
||||||
current: (fields?: string) => get<DataRow>('/api/current', fields ? { fields } : {}),
|
current: (fields?: string) => get<DataRow>('/api/current', fields ? { fields } : {}),
|
||||||
hourly: (start?: string, end?: string, fields?: string) => get<DataRow[]>('/api/hourly', { ...(start ? { start } : {}), ...(end ? { end } : {}), ...(fields ? { fields } : {}) }),
|
hourly: (start?: string, end?: string, fields?: string) => get<DataRow[]>('/api/hourly', { ...(start ? { start } : {}), ...(end ? { end } : {}), ...(fields ? { fields } : {}) }),
|
||||||
daily: (start?: string, end?: string, fields?: string) => get<DataRow[]>('/api/daily', { ...(start ? { start } : {}), ...(end ? { end } : {}), ...(fields ? { fields } : {}) }),
|
daily: (start?: Date, end?: Date, fields?: string) => get<DataRow[]>('/api/daily', {
|
||||||
|
...(start ? { start: start.toISOString().slice(0, 10) } : {}),
|
||||||
|
...(end ? { end: end.toISOString().slice(0, 10) } : {}),
|
||||||
|
...(fields ? { fields } : {})
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { ref, computed } from 'vue'
|
|
||||||
import { api, type DataRow } from './api'
|
|
||||||
|
|
||||||
export const current = ref<DataRow>({})
|
|
||||||
export const hourly = ref<DataRow[]>([])
|
|
||||||
export const daily = ref<DataRow[]>([])
|
|
||||||
export const loading = ref(false)
|
|
||||||
export const lastFetch = ref<Date | null>(null)
|
|
||||||
|
|
||||||
export async function fetchAll() {
|
|
||||||
const date = (d: Date = new Date()) => `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
const start = new Date(), end = new Date();
|
|
||||||
start.setDate(start.getDate() + 1);
|
|
||||||
end.setDate(end.getDate() + 5);
|
|
||||||
const [c, h, d] = await Promise.allSettled([api.current(), api.hourly(), api.daily(date(start), date(end))])
|
|
||||||
if (c.status === 'fulfilled') current.value = c.value
|
|
||||||
if (h.status === 'fulfilled') hourly.value = h.value
|
|
||||||
if (d.status === 'fulfilled') daily.value = d.value
|
|
||||||
lastFetch.value = new Date()
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchHistoricHourly(start: string, end: string): Promise<DataRow[]> {
|
|
||||||
return api.hourly(start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchHistoricDaily(start: string, end: string): Promise<DataRow[]> {
|
|
||||||
return api.daily(start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isDaytime = computed(() => current.value.sun_is_day === 1)
|
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
`<script setup lang="ts">
|
`<script setup lang="ts">
|
||||||
import {formatDate} from '@ztimson/utils';
|
import {formatDate} from '@ztimson/utils';
|
||||||
import {onMounted, onUnmounted, ref, computed} from 'vue';
|
import {onMounted, onUnmounted, ref} from 'vue';
|
||||||
import {current, daily, fetchAll, loading} from '../services/weather';
|
|
||||||
import {METRICS, GROUPS} from '../services/units';
|
|
||||||
import MapView from '../components/MapView.vue';
|
import MapView from '../components/MapView.vue';
|
||||||
import CurrentWeather from '../components/CurrentWeather.vue';
|
import CurrentWeather from '../components/CurrentWeather.vue';
|
||||||
import ForecastStrip from '../components/ForecastStrip.vue';
|
import ForecastStrip from '../components/ForecastStrip.vue';
|
||||||
import MetricCard from '../components/MetricCard.vue';
|
|
||||||
import GraphModal from '../components/GraphModal.vue';
|
|
||||||
|
|
||||||
const day = ref(formatDate('dddd'));
|
const day = ref(formatDate('dddd'));
|
||||||
const date = ref(formatDate('MMMM D'));
|
const date = ref(formatDate('MMMM D'));
|
||||||
const time = ref(formatDate('h:mm A'));
|
const time = ref(formatDate('h:mm A'));
|
||||||
const props = defineProps<{dark: boolean}>();
|
const props = defineProps<{dark: boolean}>();
|
||||||
const selectedMetric = ref<string | null>(null);
|
|
||||||
let interval: ReturnType<typeof setInterval>;
|
|
||||||
let clock: ReturnType<typeof setInterval>;
|
let clock: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -23,22 +17,10 @@ onMounted(async () => {
|
|||||||
date.value = formatDate('MMMM D');
|
date.value = formatDate('MMMM D');
|
||||||
time.value = formatDate('h:mm A');
|
time.value = formatDate('h:mm A');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
await fetchAll();
|
|
||||||
interval = setInterval(fetchAll, 60 * 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(clock);
|
clearInterval(clock);
|
||||||
clearInterval(interval);
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupedMetrics = computed(() => {
|
|
||||||
return GROUPS.map(group => ({
|
|
||||||
group,
|
|
||||||
keys: Object.entries(METRICS)
|
|
||||||
.filter(([key, meta]) => meta.group === group && current.value[key] != null)
|
|
||||||
.map(([key]) => key)
|
|
||||||
})).filter(g => g.keys.length > 0);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -161,8 +143,6 @@ const groupedMetrics = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<div v-if="loading" class="loading-bar"/>
|
|
||||||
|
|
||||||
<div class="map-col">
|
<div class="map-col">
|
||||||
<MapView :dark="props.dark"/>
|
<MapView :dark="props.dark"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,27 +158,14 @@ const groupedMetrics = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CurrentWeather />
|
<CurrentWeather />
|
||||||
<ForecastStrip :days="daily"/>
|
<ForecastStrip />
|
||||||
|
|
||||||
<template v-for="g in groupedMetrics" :key="g.group">
|
|
||||||
<div class="group-label">{{ g.group }}</div>
|
|
||||||
<div class="metric-grid">
|
|
||||||
<MetricCard
|
|
||||||
v-for="key in g.keys"
|
|
||||||
:key="key"
|
|
||||||
:metric-key="key"
|
|
||||||
:data="current"
|
|
||||||
@click="selectedMetric = key"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GraphModal
|
<!-- <GraphModal-->
|
||||||
:metric-key="selectedMetric"
|
<!-- :metric-key="selectedMetric"-->
|
||||||
:current-data="current"
|
<!-- :current-data="current"-->
|
||||||
@close="selectedMetric = null"
|
<!-- @close="selectedMetric = null"-->
|
||||||
/>
|
<!-- />-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -186,16 +186,6 @@ function moonriseMoonset(lat, lon, date = new Date()) {
|
|||||||
return {moonrise: moonrise, moonset: moonset};
|
return {moonrise: moonrise, moonset: moonset};
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextSolsticeEquinox(date = new Date()) {
|
|
||||||
const y = date.getFullYear();
|
|
||||||
return {
|
|
||||||
summer_solstice: new Date(Date.UTC(y, 5, 21)),
|
|
||||||
winter_solstice: new Date(Date.UTC(y, 11, 21)),
|
|
||||||
vernal_equinox: new Date(Date.UTC(y, 2, 20)),
|
|
||||||
autumnal_equinox: new Date(Date.UTC(y + 1, 2, 20)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCelestialCurrent(lat, lon, date = new Date()) {
|
export function getCelestialCurrent(lat, lon, date = new Date()) {
|
||||||
const data = {
|
const data = {
|
||||||
...sunPosition(lat, lon, date),
|
...sunPosition(lat, lon, date),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// forecast.mjs
|
// forecast.mjs
|
||||||
import {queryHourly, getCoords, queryCurrent} from './influx.mjs';
|
import {queryHourly, getCoords, queryCurrent} from './influx.mjs';
|
||||||
import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs';
|
import {getCelestialCurrent} from './celestial.mjs';
|
||||||
import {getWeatherCondition} from './openweather.mjs';
|
import {getWeatherCondition} from './openweather.mjs';
|
||||||
|
|
||||||
export let lastForecast = { ts: 0, slots: [], summary: null }
|
export let lastForecast = { ts: 0, slots: [], summary: null }
|
||||||
|
|||||||
@@ -105,37 +105,22 @@ app.get('/api/daily', async (req, res) => {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
now.setHours(0, 0, 0, 0)
|
now.setHours(0, 0, 0, 0)
|
||||||
const next = new Date(now); next.setDate(now.getDate() + 1)
|
const next = new Date(now); next.setDate(now.getDate() + 1)
|
||||||
|
|
||||||
const start = req.query.start ? new Date(req.query.start) : now
|
const start = req.query.start ? new Date(req.query.start) : now
|
||||||
const end = req.query.end ? new Date(req.query.end) : next
|
const end = req.query.end ? new Date(req.query.end) : next
|
||||||
const coords = await getCoords()
|
const coords = await getCoords()
|
||||||
|
const todayStr = now.toISOString().slice(0, 10) // ✅ string for comparison
|
||||||
// Does the requested window overlap with today (the 24h forecast window)?
|
const usePhysics = lastForecast.summary && start <= now && end > now // ✅ only when today is in window
|
||||||
const todayStr = now
|
const [sensorResult] = await Promise.allSettled([start < now ? queryDaily(start, now) : Promise.resolve([])])
|
||||||
const usePhysics = lastForecast.summary && start <= next && end >= now
|
|
||||||
|
|
||||||
// Past influx history (days before today)
|
|
||||||
const [sensorResult] = await Promise.allSettled([
|
|
||||||
start < now ? queryDaily(start, now) : Promise.resolve([])
|
|
||||||
])
|
|
||||||
const history = sensorResult.status === 'fulfilled' ? sensorResult.value : []
|
const history = sensorResult.status === 'fulfilled' ? sensorResult.value : []
|
||||||
|
|
||||||
// Future meteo (days after today)
|
// ✅ meteo starts from whichever is later: next or start
|
||||||
const meteoStart = end > next ? next : null
|
const meteoFrom = new Date(Math.max(next.getTime(), start.getTime()))
|
||||||
const [meteoResult] = await Promise.allSettled([
|
const [meteoResult] = await Promise.allSettled([end > next ? dailyWeather(coords.latitude, coords.longitude, meteoFrom, end) : Promise.resolve([])])
|
||||||
meteoStart && end > next ? dailyWeather(coords.latitude, coords.longitude, next, end) : Promise.resolve([])
|
|
||||||
])
|
|
||||||
const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
|
const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
|
||||||
|
|
||||||
// Splice in physics summary for today, skip any meteo row that duplicates it
|
|
||||||
const todaySlot = usePhysics ? [lastForecast.summary] : []
|
const todaySlot = usePhysics ? [lastForecast.summary] : []
|
||||||
const meteoClean = meteo.filter(r => r.time !== todayStr)
|
const meteoClean = meteo.filter(r => String(r.time).slice(0, 10) !== todayStr) // ✅ safe string compare
|
||||||
|
|
||||||
const daily = getCelestialForecast(
|
|
||||||
coords.latitude, coords.longitude,
|
|
||||||
[...history, ...todaySlot, ...meteoClean]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...todaySlot, ...meteoClean])
|
||||||
res.json(filterArr(daily, fields))
|
res.json(filterArr(daily, fields))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user