Weather

weather.js 91 lines

Files · 9
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091import ddl from "./schema.js";

export function conditionText(code) {
  if (code === 0) return "Clear";
  if (code >= 1 && code <= 3) return "Partly cloudy";
  if (code >= 45 && code <= 48) return "Fog";
  if (code >= 51 && code <= 55) return "Drizzle";
  if (code >= 56 && code <= 57) return "Freezing drizzle";
  if (code >= 61 && code <= 65) return "Rain";
  if (code >= 66 && code <= 67) return "Freezing rain";
  if (code >= 71 && code <= 77) return "Snow";
  if (code >= 80 && code <= 82) return "Showers";
  if (code >= 85 && code <= 86) return "Snow showers";
  if (code >= 95 && code <= 99) return "Thunderstorm";
  return "Unknown";
}

export async function setupWeather(L) {
  for (const sql of ddl.split(";").map(s => s.trim()).filter(Boolean))
    await L.db.exec(sql.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"));
}

// Observations at old coordinates cannot be restored under an edited location.
export async function updateLocation(L, previous, next) {
  await L.db.exec("BEGIN");
  try {
    await L.db.exec("UPDATE locations SET name=?,lat=?,lon=? WHERE id=?", [next.name, next.lat, next.lon, previous.id]);
    if (previous.lat !== next.lat || previous.lon !== next.lon) {
      await L.db.exec("DELETE FROM forecasts WHERE location_id=?", [previous.id]);
      await L.db.exec("DELETE FROM weather_log WHERE location_id=?", [previous.id]);
    }
    await L.db.exec("COMMIT");
  } catch (error) {
    await L.db.exec("ROLLBACK");
    throw error;
  }
}

export async function loadWeather(L, locationId) {
  const [row] = await L.db.query(
    "SELECT * FROM weather_log WHERE location_id=? ORDER BY fetched_at DESC,id DESC LIMIT 1", [locationId]);
  if (!row) return null;
  const days = await L.db.query("SELECT * FROM forecasts WHERE location_id=? ORDER BY date", [locationId]);
  return {
    current: {
      temperature_2m: row.temperature_c, apparent_temperature: row.feels_like_c,
      relative_humidity_2m: row.humidity_pct, wind_speed_10m: row.wind_speed_kmh,
      weather_code: row.weather_code, condition: row.condition,
      precipitation: row.precipitation_mm, fetched_at: row.fetched_at,
    },
    daily: {
      time: days.map(r => r.date), weather_code: days.map(r => r.weather_code),
      temperature_2m_max: days.map(r => r.temperature_max_c),
      temperature_2m_min: days.map(r => r.temperature_min_c),
      precipitation_sum: days.map(r => r.precipitation_mm),
    },
  };
}

// Call within the app's write queue. Validate the full response before replacing
// a location's forecast so failed refreshes leave its last successful data intact.
export async function saveWeather(L, locationId, payload) {
  const cur = payload?.current, daily = payload?.daily;
  const finite = v => typeof v === "number" && Number.isFinite(v);
  const fields = ["weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_sum"];
  if (!cur || !["temperature_2m", "relative_humidity_2m", "wind_speed_10m", "weather_code"].every(k => finite(cur[k])) ||
      !Array.isArray(daily?.time) || !daily.time.length ||
      !daily.time.every(d => typeof d === "string" && /^\d{4}-\d{2}-\d{2}$/.test(d)) ||
      new Set(daily.time).size !== daily.time.length ||
      !fields.every(k => Array.isArray(daily[k]) && daily[k].length === daily.time.length && daily[k].every(v => v === null || finite(v))))
    throw Error("Weather service returned incomplete data");
  const now = new Date().toISOString();
  await L.db.exec("BEGIN");
  try {
    await L.db.exec(
      "INSERT INTO weather_log (location_id,temperature_c,feels_like_c,humidity_pct,wind_speed_kmh,weather_code,condition,uv_index,precipitation_mm,fetched_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
      [locationId, cur.temperature_2m, cur.apparent_temperature ?? null, cur.relative_humidity_2m,
       cur.wind_speed_10m, cur.weather_code, conditionText(cur.weather_code), cur.uv_index ?? null, cur.precipitation ?? null, now]);
    await L.db.exec("DELETE FROM forecasts WHERE location_id=?", [locationId]);
    for (let i = 0; i < daily.time.length; i++)
      await L.db.exec(
        "INSERT INTO forecasts (location_id,date,weather_code,condition,temperature_max_c,temperature_min_c,precipitation_mm,fetched_at) VALUES (?,?,?,?,?,?,?,?)",
        [locationId, daily.time[i], daily.weather_code[i], conditionText(daily.weather_code[i]),
         daily.temperature_2m_max[i], daily.temperature_2m_min[i], daily.precipitation_sum[i], now]);
    await L.db.exec("COMMIT");
  } catch (error) {
    await L.db.exec("ROLLBACK");
    throw error;
  }
  return loadWeather(L, locationId);
}