diff --git a/.env b/.env index f18c640..4e02cdf 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ +INFLUX_TOKEN= +ALTITUDE= LATITUDE= LONGITUDE= -ALTITUDE= -INFLUX_TOKEN= +TZ= diff --git a/client/src/views/Dashboard.vue b/client/src/views/Dashboard.vue index 5db7068..b1494a1 100644 --- a/client/src/views/Dashboard.vue +++ b/client/src/views/Dashboard.vue @@ -2,6 +2,7 @@ import EnvironmentCard from '@/components/EnvironmentCard.vue'; import GraphModal from '@/components/GraphModal.vue'; import HourlyForecast from '@/components/HourlyForecast.vue'; +import {BASE} from '@/services/api.ts'; import {formatDate} from '@ztimson/utils'; import {onMounted, onUnmounted, provide, ref} from 'vue'; import MapView from '../components/MapView.vue'; @@ -49,10 +50,6 @@ onUnmounted(() => { .panel-col { width: 380px; - flex-shrink: 0; - display: flex; - flex-direction: column; - gap: 12px; padding: 16px; overflow-y: auto; border-left: 1px solid var(--border); @@ -154,7 +151,7 @@ onUnmounted(() => {
-
+

{{time}}

@@ -163,11 +160,14 @@ onUnmounted(() => {

{{date}}

- - - + + + - + +
diff --git a/server/src/openmeteo.mjs b/server/src/openmeteo.mjs index 83cea69..bbee248 100644 --- a/server/src/openmeteo.mjs +++ b/server/src/openmeteo.mjs @@ -2,7 +2,7 @@ import { createWriteStream, existsSync, mkdirSync } from 'fs' import { resolve, dirname } from 'path' import { fileURLToPath } from 'url' import { pipeline } from 'stream/promises' -import { localDateStr } from './config.mjs' +import { tzOffsetMinutes } from './config.mjs' const DIR = dirname(fileURLToPath(import.meta.url)) const ICON_DIR = resolve(DIR, 'public', 'icons') @@ -122,10 +122,10 @@ export async function dailyWeather(lat, lon, start, end) { const humidity = data.daily.relative_humidity_2m_mean[i] const uv = data.daily.uv_index_max[i] - // Open-Meteo returns local date strings — parse as local midnight using TZ - // e.g. "2026-03-01" → local midnight, not UTC midnight - const tzOffset = (new Date(new Date().toLocaleString('en-US', { timeZone: require('./config.mjs').cfg().TZ })) - new Date(new Date().toLocaleString('en-US', { timeZone: 'UTC' }))) / 60_000 - const localMidnight = new Date(new Date(`${time}T00:00:00Z`).getTime() - tzOffset * 60_000) + // "2026-03-01" → local midnight by shifting from UTC midnight by tz offset + const utcMidnight = new Date(`${time}T00:00:00Z`) + const offset = tzOffsetMinutes(utcMidnight) + const localMidnight = new Date(utcMidnight.getTime() - offset * 60_000) return { label: WMO[code] || 'Unknown', @@ -175,8 +175,8 @@ export async function hourlyWeather(lat, lon, start, end) { const data = await cachedFetch(`hourly_${lat}_${lon}_${startStr}_${endStr}`, url) if (!data?.hourly?.time) return [] - // Build sunrise/sunset map keyed by local date string from Open-Meteo - // Open-Meteo returns these as local time ISO strings e.g. "2026-03-01T06:45" + // Open-Meteo returns local time strings e.g. "2026-03-01T06:00" (no Z) + // sunMap keyed by date portion of those local strings const sunMap = {} if (data.daily?.time) { data.daily.time.forEach((date, i) => { @@ -188,14 +188,14 @@ export async function hourlyWeather(lat, lon, start, end) { } return Promise.all(data.hourly.time.map(async (time, i) => { - // Open-Meteo returns local time strings — treat as UTC-equivalent for Date parsing - // then key into sunMap using the local date portion - const slotDate = new Date(time) // parsed as local ISO (no Z suffix) — browser/node both treat naive as local - const dateKey = localDateStr(slotDate) - const sun = sunMap[dateKey] ?? {} - const t = slotDate.getTime() - const daytime = sun.sunrise && sun.sunset - ? t >= sun.sunrise.getTime() && t < sun.sunset.getTime() + const dateKey = time.slice(0, 10) // "2026-03-01" from local string + const sun = sunMap[dateKey] ?? {} + // Open-Meteo local strings have no Z — append Z to treat as UTC for ms comparison + // This works because Open-Meteo already returns times in the location's local tz + // and sunrise/sunset are parsed the same way, so the comparison is consistent + const t = new Date(time + 'Z').getTime() + const daytime = sun.sunrise && sun.sunset + ? t >= new Date(sun.sunrise.toISOString().replace('Z', '') + 'Z').getTime() && t < new Date(sun.sunset.toISOString().replace('Z', '') + 'Z').getTime() : false const code = data.hourly.weathercode[i] @@ -206,7 +206,7 @@ export async function hourlyWeather(lat, lon, start, end) { const clouds = Math.round(data.hourly.cloudcover[i]) / 100 return { - time: slotDate, + time: new Date(time + 'Z'), daytime, label: WMO[code] || 'Unknown', icon, diff --git a/server/src/server.mjs b/server/src/server.mjs index f4c296d..0a7decf 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -108,32 +108,27 @@ app.get('/api/hourly', async (req, res) => { app.get('/api/daily', async (req, res) => { const { fields } = req.query - const now = new Date() + const now = new Date() + const todayStart = new Date(now) + todayStart.setHours(0, 0, 0, 0) + const next = new Date(todayStart) + next.setDate(todayStart.getDate() + 1) - // Use local midnight as "today" boundary - const offset = tzOffsetMinutes(now) - const localNow = new Date(now.getTime() + offset * 60_000) - localNow.setUTCHours(0, 0, 0, 0) - const todayLocal = new Date(localNow.getTime() - offset * 60_000) // back to UTC - const nextLocal = new Date(todayLocal.getTime() + 24 * 3600_000) - - const start = req.query.start ? new Date(req.query.start) : todayLocal - const end = req.query.end ? new Date(req.query.end) : nextLocal - - const coords = await getCoords() + const start = req.query.start ? new Date(req.query.start) : todayStart + const end = req.query.end ? new Date(req.query.end) : next + const coords = await getCoords() const todayStr = localDateStr(now) const usePhysics = lastForecast.summary && start <= now && end > now const [sensorResult] = await Promise.allSettled([start < now ? queryDaily(start, now) : Promise.resolve([])]) const history = sensorResult.status === 'fulfilled' ? sensorResult.value : [] - const meteoFrom = new Date(Math.max(nextLocal.getTime(), start.getTime())) - const [meteoResult] = await Promise.allSettled([end > nextLocal ? dailyWeather(coords.latitude, coords.longitude, meteoFrom, end) : Promise.resolve([])]) + const meteoFrom = new Date(Math.max(next.getTime(), start.getTime())) + const [meteoResult] = await Promise.allSettled([end > next ? dailyWeather(coords.latitude, coords.longitude, meteoFrom, end) : Promise.resolve([])]) const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : [] const todaySlot = usePhysics ? [lastForecast.summary] : [] - // Filter meteo using local date string so we never double-count today const meteoClean = meteo.filter(r => localDateStr(new Date(r.time)) !== todayStr) const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...todaySlot, ...meteoClean])