129 lines
5.0 KiB
JavaScript
129 lines
5.0 KiB
JavaScript
/**
|
||
* Proxies Tomorrow.io daily weather forecast for the Plano TX area.
|
||
* Set TOMORROW_API_KEY in Vercel env vars (and .env locally for vercel dev).
|
||
*
|
||
* Response: array of 7-day daily forecast objects, normalised for the frontend.
|
||
* Cached 1 hour — free tier allows 500 calls/day.
|
||
*/
|
||
|
||
const PLANO_LAT = 33.055;
|
||
const PLANO_LNG = -96.752;
|
||
|
||
// Tomorrow.io weather code → label + hail risk flag
|
||
function interpretCode(code) {
|
||
const map = {
|
||
1000: { label: 'Clear', hailRisk: false },
|
||
1001: { label: 'Cloudy', hailRisk: false },
|
||
1100: { label: 'Mostly Clear', hailRisk: false },
|
||
1101: { label: 'Partly Cloudy', hailRisk: false },
|
||
1102: { label: 'Mostly Cloudy', hailRisk: false },
|
||
2000: { label: 'Fog', hailRisk: false },
|
||
4000: { label: 'Drizzle', hailRisk: false },
|
||
4001: { label: 'Rain', hailRisk: false },
|
||
4200: { label: 'Light Rain', hailRisk: false },
|
||
4201: { label: 'Heavy Rain', hailRisk: false },
|
||
5000: { label: 'Snow', hailRisk: false },
|
||
5001: { label: 'Flurries', hailRisk: false },
|
||
5100: { label: 'Light Snow', hailRisk: false },
|
||
5101: { label: 'Heavy Snow', hailRisk: false },
|
||
6000: { label: 'Freezing Drizzle', hailRisk: true },
|
||
6001: { label: 'Freezing Rain', hailRisk: true },
|
||
6200: { label: 'Lt Freezing Rain', hailRisk: true },
|
||
6201: { label: 'Hvy Freezing Rain',hailRisk: true },
|
||
7000: { label: 'Ice Pellets', hailRisk: true },
|
||
7101: { label: 'Heavy Hail', hailRisk: true },
|
||
7102: { label: 'Light Hail', hailRisk: true },
|
||
8000: { label: 'Thunderstorm', hailRisk: true },
|
||
};
|
||
return map[code] ?? { label: 'Mixed', hailRisk: false };
|
||
}
|
||
|
||
export default async function handler(req, res) {
|
||
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=7200');
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
if (req.method !== 'GET') {
|
||
return res.status(405).json({ error: 'Method not allowed' });
|
||
}
|
||
|
||
// TEMP DIAGNOSTIC — remove after debugging. Reports key presence (never the value).
|
||
if (req.query && req.query.debug === '1') {
|
||
const k = process.env.TOMORROW_API_KEY || '';
|
||
return res.status(200).json({
|
||
hasKey: k.length > 0,
|
||
keyLength: k.length,
|
||
expectedLength: 32,
|
||
node: process.version,
|
||
});
|
||
}
|
||
|
||
const key = process.env.TOMORROW_API_KEY;
|
||
if (!key) {
|
||
console.warn('[storm-forecast] TOMORROW_API_KEY not set');
|
||
return res.status(200).json([]);
|
||
}
|
||
|
||
try {
|
||
const fields = [
|
||
'precipitationProbability',
|
||
'weatherCode',
|
||
'windSpeedAvg',
|
||
'windGustMax',
|
||
'temperatureAvg',
|
||
'precipitationIntensityAvg',
|
||
'humidityAvg',
|
||
].join(',');
|
||
|
||
const url = `https://api.tomorrow.io/v4/weather/forecast?location=${PLANO_LAT},${PLANO_LNG}&apikey=${key}×teps=1d&units=imperial&fields=${fields}`;
|
||
const resp = await fetch(url, {
|
||
headers: { Accept: 'application/json' },
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const txt = await resp.text();
|
||
console.warn('[storm-forecast] Tomorrow.io error', resp.status, txt);
|
||
return res.status(200).json([]);
|
||
}
|
||
|
||
const data = await resp.json();
|
||
const daily = data?.timelines?.daily ?? [];
|
||
|
||
const forecast = daily.slice(0, 7).map(day => {
|
||
const v = day.values ?? {};
|
||
// Daily aggregated fields may come back as plain names or with Avg/Max suffix
|
||
const precipProb = v.precipitationProbability ?? v.precipitationProbabilityAvg ?? 0;
|
||
const weatherCode = v.weatherCode ?? v.weatherCodeMax ?? 1000;
|
||
const windGust = v.windGustMax ?? v.windGustAvg ?? 0;
|
||
const windSpeed = v.windSpeedAvg ?? 0;
|
||
const temp = v.temperatureAvg ?? 0;
|
||
const humidity = v.humidityAvg ?? 0;
|
||
const { label, hailRisk } = interpretCode(weatherCode);
|
||
|
||
// Storm severity: 0–100 score for how dangerous this day looks
|
||
const stormScore = Math.min(100, Math.round(
|
||
precipProb * 0.5 +
|
||
(hailRisk ? 35 : 0) +
|
||
(windGust > 40 ? 15 : windGust > 25 ? 8 : 0)
|
||
));
|
||
|
||
return {
|
||
date: day.time,
|
||
precipProb: Math.round(precipProb),
|
||
weatherCode,
|
||
weatherLabel: label,
|
||
hailRisk,
|
||
windGust: Math.round(windGust),
|
||
windSpeed: Math.round(windSpeed),
|
||
tempF: Math.round(temp),
|
||
humidity: Math.round(humidity),
|
||
stormScore,
|
||
};
|
||
});
|
||
|
||
return res.status(200).json(forecast);
|
||
} catch (err) {
|
||
console.error('[storm-forecast] error:', err.message);
|
||
return res.status(200).json([]);
|
||
}
|
||
}
|