Weather Station / ECOWITT / DNT
0

Configure Feed

Select the types of activity you want to include in your feed.

forcast analysis glitch 3.0

+210 -109
+33
check_db.ts
··· 1 + 2 + import { getDuckConn } from "./src/lib/db/duckdb"; 3 + 4 + async function check() { 5 + try { 6 + const conn = await getDuckConn(); 7 + 8 + console.log("--- Forecasts Storage Dates ---"); 9 + const forecasts = await conn.runAndReadAll(` 10 + SELECT DISTINCT storage_date 11 + FROM forecasts 12 + ORDER BY storage_date DESC 13 + LIMIT 20 14 + `); 15 + const fRows = forecasts.getRowObjects(); 16 + console.table(fRows); 17 + 18 + console.log("\n--- Analysis Dates ---"); 19 + const analysis = await conn.runAndReadAll(` 20 + SELECT DISTINCT analysis_date 21 + FROM forecast_analysis 22 + ORDER BY analysis_date DESC 23 + LIMIT 20 24 + `); 25 + const aRows = analysis.getRowObjects(); 26 + console.table(aRows); 27 + 28 + } catch (e) { 29 + console.error(e); 30 + } 31 + } 32 + 33 + check();
+177 -109
src/instrumentation.ts
··· 39 39 try { 40 40 const { setLastRealtime } = await import("@/lib/realtimeArchiver"); 41 41 await setLastRealtime({ ok: false, updatedAt: new Date().toISOString(), error: msg }); 42 - } catch {} 42 + } catch { } 43 43 } 44 44 })(); 45 45 ··· 54 54 try { 55 55 const { setLastRealtime } = await import("@/lib/realtimeArchiver"); 56 56 await setLastRealtime({ ok: false, updatedAt: new Date().toISOString(), error: msg }); 57 - } catch {} 57 + } catch { } 58 58 } 59 59 }, intervalMs); 60 60 } ··· 88 88 // Schedule daily forecast storage at 20:00 (8 PM) 89 89 if (!global.__forecastPoller) { 90 90 const stationSetting = process.env.FORECAST_STATION_ID || "11035"; // or 'ALL' 91 - console.log(`[forecast] Daily forecast storage enabled for ${stationSetting === 'ALL' ? 'ALL stations' : `station ${stationSetting}`} (runs at 20:00 daily)`); 91 + console.log(`[forecast] Daily forecast storage enabled for ${stationSetting === 'ALL' ? 'ALL stations' : `station ${stationSetting}`} (runs at 20:00 daily + catchup)`); 92 92 93 - let lastRunDate: string | null = null; 94 - 95 - // Check every 10 minutes if it's between 20:00 and 20:30 93 + let lastScheduledRunDate: string | null = null; 94 + let lastCatchupRunDate: string | null = null; 95 + 96 + // Check every 10 minutes 96 97 global.__forecastPoller = setInterval(async () => { 97 98 const now = new Date(); 98 99 const currentDate = now.toISOString().split('T')[0]; 99 100 const currentHour = now.getHours(); 100 101 const currentMinute = now.getMinutes(); 101 - 102 - // Run between 20:00 and 20:30 and only once per day 103 - if (currentHour === 20 && currentMinute <= 30 && lastRunDate !== currentDate) { 102 + 103 + const isScheduledWindow = currentHour === 20 && currentMinute <= 30; 104 + 105 + // 1. Scheduled Run (20:00 - 20:30) 106 + if (isScheduledWindow && lastScheduledRunDate !== currentDate) { 104 107 console.log(`[forecast] ========================================`); 105 - console.log(`[forecast] DAILY POLLER TRIGGERED at ${now.toISOString()}`); 108 + console.log(`[forecast] SCHEDULED POLLER TRIGGERED at ${now.toISOString()}`); 106 109 console.log(`[forecast] ========================================`); 107 - 108 - lastRunDate = currentDate; 109 - 110 + 111 + lastScheduledRunDate = currentDate; 112 + // Also mark catchup as done to avoid double run if we just started 113 + lastCatchupRunDate = currentDate; 114 + 115 + await runForecastJob(stationSetting); 116 + return; 117 + } 118 + 119 + // 2. Catchup Run (If no data exists for today and we haven't checked recently) 120 + // Only check if we haven't already run a catchup or scheduled run today 121 + if (lastCatchupRunDate !== currentDate && lastScheduledRunDate !== currentDate) { 110 122 try { 111 - // Resolve station list 112 - let stationIds: string[] = []; 113 - if (stationSetting === 'ALL') { 114 - try { 115 - // Fetch station list directly from Geosphere API 116 - const res = await fetch('https://dataset.api.hub.geosphere.at/v1/station/current/tawes-v1-10min/metadata'); 117 - if (res.ok) { 118 - const data = await res.json(); 119 - stationIds = (data.stations || []).map((s: any) => String(s.id)); 120 - } 121 - } catch (e) { 122 - console.error('[forecast] Failed to load station list for ALL:', e); 123 - } 123 + const { getDuckConn } = await import("@/lib/db/duckdb"); 124 + const conn = await getDuckConn(); 125 + 126 + // Check if we have ANY forecast data for today 127 + // We use a simple query to check existence 128 + const checkQuery = `SELECT 1 FROM forecasts WHERE storage_date = '${currentDate}' LIMIT 1`; 129 + const result = await conn.runAndReadAll(checkQuery); 130 + const hasData = result.getRowObjects().length > 0; 131 + 132 + if (!hasData) { 133 + console.log(`[forecast] ========================================`); 134 + console.log(`[forecast] CATCHUP POLLER TRIGGERED at ${now.toISOString()}`); 135 + console.log(`[forecast] No forecast data found for today (${currentDate}). Running catchup...`); 136 + console.log(`[forecast] ========================================`); 137 + 138 + lastCatchupRunDate = currentDate; 139 + await runForecastJob(stationSetting); 140 + } else { 141 + // We have data, so mark catchup as done for today to avoid checking DB constantly 142 + // But DO NOT set lastScheduledRunDate, so the 20:00 run can still happen 143 + lastCatchupRunDate = currentDate; 144 + console.log(`[forecast] Data already exists for ${currentDate}. Catchup skipped.`); 124 145 } 125 - if (!stationIds.length) stationIds = [String(stationSetting)]; 146 + } catch (e) { 147 + console.error("[forecast] Catchup check failed:", e); 148 + } 149 + } 150 + }, 3600000); // Check every hour (3600000 ms) 126 151 127 - console.log(`[forecast] Processing ${stationIds.length} station(s)...`); 152 + console.log(`[forecast] Poller active: checking every hour`); 128 153 129 - for (const sid of stationIds) { 130 - try { 131 - console.log(`[forecast] → Station ${sid}: Storing forecasts...`); 132 - await storeForecastForStation(sid); 133 - 134 - console.log(`[forecast] → Station ${sid}: Calculating analysis...`); 135 - await calculateAndStoreDailyAnalysis(sid); 136 - console.log(`[forecast] → Station ${sid}: Backfilling analysis for last 7 days...`); 137 - await backfillForecastAnalysis(sid, 7); 154 + // Trigger an immediate check on startup (after 10s delay to let DB settle) 155 + setTimeout(async () => { 156 + console.log("[forecast] Running startup check..."); 157 + try { 158 + const { getDuckConn } = await import("@/lib/db/duckdb"); 159 + const conn = await getDuckConn(); 160 + const now = new Date(); 161 + const currentDate = now.toISOString().split('T')[0]; 138 162 139 - console.log(`[forecast] ✓ Station ${sid}: Complete`); 140 - } catch (e: any) { 141 - console.error(`[forecast] ✗ Station ${sid} failed:`, e?.message || e); 142 - } 163 + const checkQuery = `SELECT 1 FROM forecasts WHERE storage_date = '${currentDate}' LIMIT 1`; 164 + const result = await conn.runAndReadAll(checkQuery); 165 + const hasData = result.getRowObjects().length > 0; 143 166 144 - // Small delay to be gentle on upstream APIs 145 - await new Promise(r => setTimeout(r, 250)); 146 - } 147 - 148 - console.log(`[forecast] ========================================`); 149 - console.log(`[forecast] DAILY POLLER COMPLETE`); 150 - console.log(`[forecast] ========================================`); 151 - } catch (e: any) { 152 - console.error("[forecast] Daily storage failed:", e?.message || e); 167 + if (!hasData) { 168 + console.log(`[forecast] Startup catchup triggered for ${currentDate}`); 169 + lastCatchupRunDate = currentDate; 170 + await runForecastJob(stationSetting); 171 + } else { 172 + console.log(`[forecast] Startup check: Data exists for ${currentDate}`); 173 + lastCatchupRunDate = currentDate; 153 174 } 175 + } catch (e) { 176 + console.error("[forecast] Startup check failed:", e); 154 177 } 155 - }, 600000); // Check every 10 minutes (600000 ms) 156 - 157 - console.log(`[forecast] Poller active: checking every 10 minutes for 20:00 window (20:00-20:30)`); 178 + }, 10000); 179 + } 180 + } 181 + 182 + async function runForecastJob(stationSetting: string) { 183 + try { 184 + // Resolve station list 185 + let stationIds: string[] = []; 186 + if (stationSetting === 'ALL') { 187 + try { 188 + // Fetch station list directly from Geosphere API 189 + const res = await fetch('https://dataset.api.hub.geosphere.at/v1/station/current/tawes-v1-10min/metadata'); 190 + if (res.ok) { 191 + const data = await res.json(); 192 + stationIds = (data.stations || []).map((s: any) => String(s.id)); 193 + } 194 + } catch (e) { 195 + console.error('[forecast] Failed to load station list for ALL:', e); 196 + } 197 + } 198 + if (!stationIds.length) stationIds = [String(stationSetting)]; 199 + 200 + console.log(`[forecast] Processing ${stationIds.length} station(s)...`); 201 + 202 + for (const sid of stationIds) { 203 + try { 204 + console.log(`[forecast] → Station ${sid}: Storing forecasts...`); 205 + await storeForecastForStation(sid); 206 + 207 + console.log(`[forecast] → Station ${sid}: Calculating analysis...`); 208 + await calculateAndStoreDailyAnalysis(sid); 209 + console.log(`[forecast] → Station ${sid}: Backfilling analysis for last 7 days...`); 210 + await backfillForecastAnalysis(sid, 7); 211 + 212 + console.log(`[forecast] ✓ Station ${sid}: Complete`); 213 + } catch (e: any) { 214 + console.error(`[forecast] ✗ Station ${sid} failed:`, e?.message || e); 215 + } 216 + 217 + // Small delay to be gentle on upstream APIs 218 + await new Promise(r => setTimeout(r, 250)); 219 + } 220 + 221 + console.log(`[forecast] ========================================`); 222 + console.log(`[forecast] JOB COMPLETE`); 223 + console.log(`[forecast] ========================================`); 224 + } catch (e: any) { 225 + console.error("[forecast] Job failed:", e?.message || e); 158 226 } 159 227 } 160 228 ··· 165 233 console.log(`[forecast-store] ========================================`); 166 234 console.log(`[forecast-store] START: Storing forecasts for station ${stationId}`); 167 235 console.log(`[forecast-store] ========================================`); 168 - 236 + 169 237 try { 170 238 const { getDuckConn } = await import("@/lib/db/duckdb"); 171 239 const conn = await getDuckConn(); 172 240 const storageDate = new Date().toISOString().split('T')[0]; 173 241 console.log(`[forecast-store] ✓ Database connection established`); 174 - 242 + 175 243 // Create forecast table if not exists 176 244 await conn.run(` 177 245 CREATE TABLE IF NOT EXISTS forecasts ( ··· 193 261 // Get station coordinates first 194 262 console.log(`[forecast-store] Fetching station metadata...`); 195 263 const stationsResponse = await fetch('https://dataset.api.hub.geosphere.at/v1/station/current/tawes-v1-10min/metadata'); 196 - 264 + 197 265 if (!stationsResponse.ok) { 198 266 throw new Error(`Failed to fetch station metadata: ${stationsResponse.status} ${stationsResponse.statusText}`); 199 267 } 200 - 268 + 201 269 const stationsData = await stationsResponse.json(); 202 270 const station = stationsData.stations.find((s: any) => s.id === stationId); 203 - 271 + 204 272 if (!station) { 205 273 throw new Error(`Station ${stationId} not found in metadata`); 206 274 } 207 - 275 + 208 276 console.log(`[forecast-store] ✓ Station found: ${station.name} (${station.lat}, ${station.lon})`); 209 - 210 - 277 + 278 + 211 279 const lat = station.lat; 212 280 const lon = station.lon; 213 - 281 + 214 282 // Fetch forecasts from all 4 sources - DIRECTLY from external APIs 215 283 const sources = ['geosphere', 'openweather', 'meteoblue', 'openmeteo']; 216 - 284 + 217 285 for (const sourceName of sources) { 218 286 try { 219 287 console.log(`[forecast-store] Processing source: ${sourceName}`); 220 288 let forecastData: any[] = []; 221 - 289 + 222 290 // Fetch from external API directly 223 291 if (sourceName === 'geosphere') { 224 292 // CRITICAL: Geosphere forecast API uses lat_lon, NOT station_ids! (station_ids returns 422 error) ··· 232 300 } 233 301 const data = await res.json(); 234 302 console.log(`[forecast-store] Geosphere data: ${data.timestamps?.length} timestamps, ${data.features?.length} features`); 235 - 303 + 236 304 // Process Geosphere hourly data 237 305 if (data && data.features && data.features.length > 0 && data.timestamps) { 238 306 const feature = data.features[0]; ··· 242 310 const uWindData = feature.properties.parameters.u10m_p50?.data || []; 243 311 const vWindData = feature.properties.parameters.v10m_p50?.data || []; 244 312 const timestamps = data.timestamps || []; 245 - 313 + 246 314 tempData.forEach((tempValue: any, index: number) => { 247 315 if (index < timestamps.length) { 248 316 const time = timestamps[index]; ··· 263 331 }); 264 332 } 265 333 } 266 - 334 + 267 335 // Aggregate hourly to daily 268 336 const dailyData = aggregateHourlyToDaily(forecastData); 269 337 console.log(`[forecast-store] Geosphere: ${forecastData.length} hourly rows → ${dailyData.length} daily rows`); 270 - 338 + 271 339 for (const day of dailyData) { 272 340 await conn.run(` 273 341 INSERT INTO forecasts ··· 280 348 console.log(`[forecast-store] ✓ Inserted Geosphere for ${day.date}`); 281 349 } 282 350 console.log(`[forecast-store] ✓ Geosphere complete: ${dailyData.length} days stored`); 283 - 351 + 284 352 } else if (sourceName === 'openweather') { 285 353 const apiKey = process.env.OPENWEATHER_API_KEY; 286 354 if (!apiKey) continue; 287 - 355 + 288 356 const res = await fetch(`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&units=metric&appid=${apiKey}`); 289 357 if (!res.ok) continue; 290 358 const data = await res.json(); 291 - 359 + 292 360 // Process OpenWeather 3-hour data to daily 293 361 const dailyMap: Record<string, any[]> = {}; 294 362 data.list?.forEach((item: any) => { ··· 297 365 if (!dailyMap[dateKey]) dailyMap[dateKey] = []; 298 366 dailyMap[dateKey].push(item); 299 367 }); 300 - 368 + 301 369 forecastData = Object.entries(dailyMap).map(([dateKey, items]) => { 302 370 const temps = items.map((i: any) => i.main.temp); 303 371 const tempMins = items.map((i: any) => i.main.temp_min); ··· 305 373 const precipitations = items.map((i: any) => (i.rain?.['3h'] ?? 0) + (i.snow?.['3h'] ?? 0)); 306 374 const windSpeeds = items.map((i: any) => i.wind.speed); 307 375 const windGusts = items.map((i: any) => i.wind.gust ?? 0); 308 - 376 + 309 377 return { 310 378 date: new Date(dateKey + 'T12:00:00').toISOString(), 311 379 tempMin: Math.min(...tempMins), ··· 315 383 windGust: Math.max(...windGusts) * 3.6 316 384 }; 317 385 }); 318 - 386 + 319 387 for (const day of forecastData) { 320 388 await conn.run(` 321 389 INSERT INTO forecasts ··· 327 395 wind_gust = EXCLUDED.wind_gust 328 396 `, [storageDate, stationId, day.date, 'openweather', day.tempMin, day.tempMax, day.precipitation, day.windSpeed, day.windGust]); 329 397 } 330 - 398 + 331 399 } else if (sourceName === 'meteoblue') { 332 400 const apiKey = process.env.METEOBLUE_API_KEY; 333 401 if (!apiKey) continue; 334 - 402 + 335 403 const res = await fetch(`https://my.meteoblue.com/packages/basic-day?apikey=${apiKey}&lat=${lat}&lon=${lon}&asl=500&format=json&temperature=C&windspeed=kmh&precipitationamount=mm&timeformat=iso8601`); 336 404 if (!res.ok) continue; 337 405 const data = await res.json(); 338 - 406 + 339 407 if (data.data_day) { 340 408 const d = data.data_day; 341 409 const timeArray = d.time || []; ··· 344 412 const precipArray = d.precipitation || []; 345 413 const windSpeedArray = d.windspeed_mean || []; 346 414 const windGustArray = d.windspeed_max || []; 347 - 415 + 348 416 forecastData = []; 349 417 for (let i = 0; i < Math.min(7, timeArray.length); i++) { 350 418 forecastData.push({ ··· 357 425 }); 358 426 } 359 427 } 360 - 428 + 361 429 for (const day of forecastData) { 362 430 await conn.run(` 363 431 INSERT INTO forecasts ··· 369 437 wind_gust = EXCLUDED.wind_gust 370 438 `, [storageDate, stationId, day.date, 'meteoblue', day.tempMin, day.tempMax, day.precipitation, day.windSpeed, day.windGust]); 371 439 } 372 - 440 + 373 441 } else if (sourceName === 'openmeteo') { 374 442 const res = await fetch(`https://api.open-meteo.com/v1/dwd-icon?latitude=${lat}&longitude=${lon}&daily=temperature_2m_max,temperature_2m_min,temperature_2m_mean,precipitation_sum,windspeed_10m_max,windgusts_10m_max,weathercode&timezone=Europe%2FBerlin&forecast_days=7`); 375 443 if (!res.ok) continue; 376 444 const data = await res.json(); 377 - 445 + 378 446 if (data.daily) { 379 447 const d = data.daily; 380 448 const timeArray = d.time || []; ··· 383 451 const precipArray = d.precipitation_sum || []; 384 452 const windSpeedArray = d.windspeed_10m_max || []; 385 453 const windGustArray = d.windgusts_10m_max || []; 386 - 454 + 387 455 forecastData = []; 388 456 for (let i = 0; i < timeArray.length; i++) { 389 457 forecastData.push({ ··· 396 464 }); 397 465 } 398 466 } 399 - 467 + 400 468 for (const day of forecastData) { 401 469 await conn.run(` 402 470 INSERT INTO forecasts ··· 413 481 console.error(`[forecast-store] ✗ Failed to store ${sourceName}:`, e?.message || e); 414 482 } 415 483 } 416 - 484 + 417 485 console.log(`[forecast-store] ========================================`); 418 486 console.log(`[forecast-store] DONE: Forecasts stored for station ${stationId}`); 419 487 console.log(`[forecast-store] ========================================`); ··· 432 500 */ 433 501 function aggregateHourlyToDaily(hourlyData: any[]): any[] { 434 502 const dailyMap: Record<string, any[]> = {}; 435 - 503 + 436 504 hourlyData.forEach(item => { 437 505 const date = new Date(item.time).toISOString().split('T')[0]; 438 506 if (!dailyMap[date]) { ··· 445 513 const temps = items.map(i => i.temperature).filter(t => t !== null); 446 514 const precipitations = items.map(i => i.precipitation).filter(p => p !== null); 447 515 const windSpeeds = items.map(i => i.windSpeed).filter(w => w !== null); 448 - 516 + 449 517 return { 450 518 date, 451 519 tempMin: temps.length > 0 ? Math.min(...temps) : null, ··· 464 532 console.log(`[forecast-analysis] ========================================`); 465 533 console.log(`[forecast-analysis] START: Calculating analysis for station ${stationId}`); 466 534 console.log(`[forecast-analysis] ========================================`); 467 - 535 + 468 536 try { 469 537 const { getDuckConn } = await import("@/lib/db/duckdb"); 470 538 const conn = await getDuckConn(); 471 539 console.log(`[forecast-analysis] ✓ Database connection established`); 472 - 540 + 473 541 // Create analysis table if not exists 474 542 await conn.run(` 475 543 CREATE TABLE IF NOT EXISTS forecast_analysis ( ··· 493 561 PRIMARY KEY(analysis_date, station_id, forecast_date, source) 494 562 ) 495 563 `); 496 - 564 + 497 565 console.log(`[forecast-analysis] ✓ Analysis table created/verified`); 498 - 566 + 499 567 // Delete old analysis data (older than 90 days) 500 568 /* 501 569 await conn.run(` ··· 504 572 `); 505 573 console.log(`[forecast-analysis] ✓ Cleaned up old analysis records (>90 days)`); 506 574 */ 507 - 575 + 508 576 // Analyze YESTERDAY's weather vs forecasts that were stored for YESTERDAY 509 577 // We use YESTERDAY because historical data has a 1-2 day delay 510 578 const yesterday = new Date(targetDate); ··· 533 601 // Query daily aggregates directly from Parquet with robust column detection 534 602 const qp = parquetFiles.map((p) => p.replace(/\\/g, "/")); 535 603 const cols = await discoverMainColumns(qp); 536 - 604 + 537 605 if (!cols.temp) { 538 606 console.warn(`[forecast-analysis] ✗ Could not detect temperature column in MAIN data`); 539 607 return; ··· 553 621 const windExpr = windExprList.length ? `COALESCE(${windExprList.join(', ')})` : 'NULL'; 554 622 555 623 const arr = '[' + qp.map((p) => `'${p}'`).join(',') + ']'; 556 - 624 + 557 625 // Build rain aggregation based on mode (daily cumulative vs hourly/generic sum) 558 626 let rainAggExpr = 'NULL'; 559 627 if (cols.rainMode === 'daily' && rainDailyExprList.length) { ··· 563 631 } else if (rainGenericExprList.length) { 564 632 rainAggExpr = 'sum(rain_g)'; 565 633 } 566 - 634 + 567 635 const sql = ` 568 636 WITH src AS ( 569 637 SELECT * FROM read_parquet(${arr}, union_by_name=true) ··· 635 703 AND storage_date <= '${yesterdayStr}' 636 704 ORDER BY storage_date DESC, source 637 705 `; 638 - 706 + 639 707 console.log(`[forecast-analysis] Querying forecasts from DB...`); 640 708 console.log(`[forecast-analysis] Query:`, forecastQuery.trim()); 641 - 709 + 642 710 const forecastReader = await conn.runAndReadAll(forecastQuery); 643 711 const forecasts: any = forecastReader.getRowObjects(); 644 - 712 + 645 713 console.log(`[forecast-analysis] Found ${forecasts.length} forecast rows for YESTERDAY (${yesterdayStr})`); 646 - 714 + 647 715 if (forecasts.length === 0) { 648 716 console.warn(`[forecast-analysis] ✗ No forecasts found for YESTERDAY (${yesterdayStr}) in database`); 649 717 console.warn(`[forecast-analysis] This means no forecasts were stored BEFORE yesterday for yesterday`); 650 718 return; 651 719 } 652 - 720 + 653 721 // Take the latest (by storage_date DESC) forecast per source only 654 722 const latestBySource: Record<string, any> = {}; 655 723 for (const f of forecasts) { ··· 657 725 latestBySource[f.source] = f; 658 726 } 659 727 } 660 - 728 + 661 729 console.log(`[forecast-analysis] Latest forecasts by source:`, Object.keys(latestBySource)); 662 730 console.log(`[forecast-analysis] Details:`, JSON.stringify(latestBySource, null, 2)); 663 - 731 + 664 732 // Store analysis for each source once 665 733 let stored = 0; 666 734 for (const forecast of Object.values(latestBySource)) { 667 - const tempMinError = actualConverted.tempMin !== null && forecast.temp_min !== null 735 + const tempMinError = actualConverted.tempMin !== null && forecast.temp_min !== null 668 736 ? Math.abs(actualConverted.tempMin - forecast.temp_min) : null; 669 - const tempMaxError = actualConverted.tempMax !== null && forecast.temp_max !== null 737 + const tempMaxError = actualConverted.tempMax !== null && forecast.temp_max !== null 670 738 ? Math.abs(actualConverted.tempMax - forecast.temp_max) : null; 671 - const precipitationError = actualConverted.precipitation !== null && forecast.precipitation !== null 739 + const precipitationError = actualConverted.precipitation !== null && forecast.precipitation !== null 672 740 ? Math.abs(actualConverted.precipitation - forecast.precipitation) : null; 673 - const windSpeedError = actualConverted.windSpeed !== null && forecast.wind_speed !== null 741 + const windSpeedError = actualConverted.windSpeed !== null && forecast.wind_speed !== null 674 742 ? Math.abs(actualConverted.windSpeed - forecast.wind_speed) : null; 675 - 743 + 676 744 console.log(`[forecast-analysis] Storing analysis for source: ${forecast.source}`); 677 745 console.log(`[forecast-analysis] Errors: TMin=${tempMinError?.toFixed(2)}, TMax=${tempMaxError?.toFixed(2)}, Precip=${precipitationError?.toFixed(2)}, Wind=${windSpeedError?.toFixed(2)}`); 678 - 746 + 679 747 await conn.run(` 680 748 INSERT INTO forecast_analysis 681 749 (analysis_date, station_id, forecast_date, source, ··· 703 771 actualConverted.tempMin, actualConverted.tempMax, actualConverted.precipitation, actualConverted.windSpeed, 704 772 forecast.temp_min, forecast.temp_max, forecast.precipitation, forecast.wind_speed 705 773 ]); 706 - 774 + 707 775 stored++; 708 776 } 709 - 777 + 710 778 console.log(`[forecast-analysis] ✓ Successfully stored ${stored} analysis records for YESTERDAY (${yesterdayStr})`); 711 779 console.log(`[forecast-analysis] ========================================`); 712 780 console.log(`[forecast-analysis] DONE`);