import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; export const dynamic = 'force-dynamic'; // In-memory cache: { key: { data, fetchedAt } } const weatherCache = new Map(); const CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes const CACHE_MAX_ENTRIES = 500; // one entry per user/date-range — bound the Map function evictStaleEntries() { const now = Date.now(); for (const [key, entry] of weatherCache) { if (now - entry.fetchedAt >= CACHE_TTL_MS) weatherCache.delete(key); } // Still over budget after TTL eviction: drop oldest entries // (Map iterates in insertion order). while (weatherCache.size >= CACHE_MAX_ENTRIES) { const oldest = weatherCache.keys().next().value; if (oldest === undefined) break; weatherCache.delete(oldest); } } export async function GET(request: NextRequest) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const user = await prisma.user.findUnique({ where: { email: session.user.email }, select: { weatherEnabled: true, weatherLat: true, weatherLon: true }, }); if (!user?.weatherLat || !user.weatherLon) { return NextResponse.json({ hourly: {} }); } const { searchParams } = new URL(request.url); const startDate = searchParams.get('start') || new Date().toISOString().slice(0, 10); const endDate = searchParams.get('end') || startDate; const cacheKey = `${user.weatherLat},${user.weatherLon},${startDate},${endDate}`; const cached = weatherCache.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { return NextResponse.json(cached.data); } try { const url = `https://api.open-meteo.com/v1/forecast?latitude=${user.weatherLat}&longitude=${user.weatherLon}&hourly=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,wind_gusts_10m,precipitation_probability,precipitation,relative_humidity_2m,uv_index&start_date=${startDate}&end_date=${endDate}&timezone=auto`; const res = await fetch(url, { next: { revalidate: 900 } }); if (!res.ok) { // Silently return empty data (e.g. date out of forecast range) return NextResponse.json({ hourly: {} }); } const raw = await res.json(); const hourly: Record = {}; if (raw.hourly?.time) { for (let i = 0; i < raw.hourly.time.length; i++) { hourly[raw.hourly.time[i]] = { temp: Math.round(raw.hourly.temperature_2m?.[i] ?? 0), code: raw.hourly.weather_code?.[i] ?? 0, feelsLike: raw.hourly.apparent_temperature?.[i] != null ? Math.round(raw.hourly.apparent_temperature[i]) : undefined, wind: raw.hourly.wind_speed_10m?.[i] != null ? Math.round(raw.hourly.wind_speed_10m[i]) : undefined, gusts: raw.hourly.wind_gusts_10m?.[i] != null ? Math.round(raw.hourly.wind_gusts_10m[i]) : undefined, precipProb: raw.hourly.precipitation_probability?.[i] ?? undefined, precip: raw.hourly.precipitation?.[i] != null ? Math.round(raw.hourly.precipitation[i] * 10) / 10 : undefined, humidity: raw.hourly.relative_humidity_2m?.[i] ?? undefined, uv: raw.hourly.uv_index?.[i] != null ? Math.round(raw.hourly.uv_index[i] * 10) / 10 : undefined, }; } } const data = { hourly, timezone: raw.timezone }; evictStaleEntries(); weatherCache.set(cacheKey, { data, fetchedAt: Date.now() }); return NextResponse.json(data); } catch (err) { console.error('[WEATHER] Fetch failed:', err); return NextResponse.json({ error: 'Weather fetch failed' }, { status: 500 }); } }