alpha
Login
or
Join now
jeamy.tngl.sh
/
ecowitt
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Weather Station / ECOWITT / DNT
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
forcast analysis glitch 3.0
author
Jeamy
date
7 months ago
(Nov 24, 2025, 9:36 PM +0100)
commit
0525bbf9
0525bbf96316b5e96eda0492289ed44930b32b4e
parent
fce9a984
fce9a9848f729c7f2f6d026896693713af3bf288
+210
-109
2 changed files
Expand all
Collapse all
Unified
Split
check_db.ts
src
instrumentation.ts
+33
check_db.ts
Reviewed
···
1
1
+
2
2
+
import { getDuckConn } from "./src/lib/db/duckdb";
3
3
+
4
4
+
async function check() {
5
5
+
try {
6
6
+
const conn = await getDuckConn();
7
7
+
8
8
+
console.log("--- Forecasts Storage Dates ---");
9
9
+
const forecasts = await conn.runAndReadAll(`
10
10
+
SELECT DISTINCT storage_date
11
11
+
FROM forecasts
12
12
+
ORDER BY storage_date DESC
13
13
+
LIMIT 20
14
14
+
`);
15
15
+
const fRows = forecasts.getRowObjects();
16
16
+
console.table(fRows);
17
17
+
18
18
+
console.log("\n--- Analysis Dates ---");
19
19
+
const analysis = await conn.runAndReadAll(`
20
20
+
SELECT DISTINCT analysis_date
21
21
+
FROM forecast_analysis
22
22
+
ORDER BY analysis_date DESC
23
23
+
LIMIT 20
24
24
+
`);
25
25
+
const aRows = analysis.getRowObjects();
26
26
+
console.table(aRows);
27
27
+
28
28
+
} catch (e) {
29
29
+
console.error(e);
30
30
+
}
31
31
+
}
32
32
+
33
33
+
check();
+177
-109
src/instrumentation.ts
Reviewed
···
39
39
try {
40
40
const { setLastRealtime } = await import("@/lib/realtimeArchiver");
41
41
await setLastRealtime({ ok: false, updatedAt: new Date().toISOString(), error: msg });
42
42
-
} catch {}
42
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
57
-
} catch {}
57
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
91
-
console.log(`[forecast] Daily forecast storage enabled for ${stationSetting === 'ALL' ? 'ALL stations' : `station ${stationSetting}`} (runs at 20:00 daily)`);
91
91
+
console.log(`[forecast] Daily forecast storage enabled for ${stationSetting === 'ALL' ? 'ALL stations' : `station ${stationSetting}`} (runs at 20:00 daily + catchup)`);
92
92
93
93
-
let lastRunDate: string | null = null;
94
94
-
95
95
-
// Check every 10 minutes if it's between 20:00 and 20:30
93
93
+
let lastScheduledRunDate: string | null = null;
94
94
+
let lastCatchupRunDate: string | null = null;
95
95
+
96
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
101
-
102
102
-
// Run between 20:00 and 20:30 and only once per day
103
103
-
if (currentHour === 20 && currentMinute <= 30 && lastRunDate !== currentDate) {
102
102
+
103
103
+
const isScheduledWindow = currentHour === 20 && currentMinute <= 30;
104
104
+
105
105
+
// 1. Scheduled Run (20:00 - 20:30)
106
106
+
if (isScheduledWindow && lastScheduledRunDate !== currentDate) {
104
107
console.log(`[forecast] ========================================`);
105
105
-
console.log(`[forecast] DAILY POLLER TRIGGERED at ${now.toISOString()}`);
108
108
+
console.log(`[forecast] SCHEDULED POLLER TRIGGERED at ${now.toISOString()}`);
106
109
console.log(`[forecast] ========================================`);
107
107
-
108
108
-
lastRunDate = currentDate;
109
109
-
110
110
+
111
111
+
lastScheduledRunDate = currentDate;
112
112
+
// Also mark catchup as done to avoid double run if we just started
113
113
+
lastCatchupRunDate = currentDate;
114
114
+
115
115
+
await runForecastJob(stationSetting);
116
116
+
return;
117
117
+
}
118
118
+
119
119
+
// 2. Catchup Run (If no data exists for today and we haven't checked recently)
120
120
+
// Only check if we haven't already run a catchup or scheduled run today
121
121
+
if (lastCatchupRunDate !== currentDate && lastScheduledRunDate !== currentDate) {
110
122
try {
111
111
-
// Resolve station list
112
112
-
let stationIds: string[] = [];
113
113
-
if (stationSetting === 'ALL') {
114
114
-
try {
115
115
-
// Fetch station list directly from Geosphere API
116
116
-
const res = await fetch('https://dataset.api.hub.geosphere.at/v1/station/current/tawes-v1-10min/metadata');
117
117
-
if (res.ok) {
118
118
-
const data = await res.json();
119
119
-
stationIds = (data.stations || []).map((s: any) => String(s.id));
120
120
-
}
121
121
-
} catch (e) {
122
122
-
console.error('[forecast] Failed to load station list for ALL:', e);
123
123
-
}
123
123
+
const { getDuckConn } = await import("@/lib/db/duckdb");
124
124
+
const conn = await getDuckConn();
125
125
+
126
126
+
// Check if we have ANY forecast data for today
127
127
+
// We use a simple query to check existence
128
128
+
const checkQuery = `SELECT 1 FROM forecasts WHERE storage_date = '${currentDate}' LIMIT 1`;
129
129
+
const result = await conn.runAndReadAll(checkQuery);
130
130
+
const hasData = result.getRowObjects().length > 0;
131
131
+
132
132
+
if (!hasData) {
133
133
+
console.log(`[forecast] ========================================`);
134
134
+
console.log(`[forecast] CATCHUP POLLER TRIGGERED at ${now.toISOString()}`);
135
135
+
console.log(`[forecast] No forecast data found for today (${currentDate}). Running catchup...`);
136
136
+
console.log(`[forecast] ========================================`);
137
137
+
138
138
+
lastCatchupRunDate = currentDate;
139
139
+
await runForecastJob(stationSetting);
140
140
+
} else {
141
141
+
// We have data, so mark catchup as done for today to avoid checking DB constantly
142
142
+
// But DO NOT set lastScheduledRunDate, so the 20:00 run can still happen
143
143
+
lastCatchupRunDate = currentDate;
144
144
+
console.log(`[forecast] Data already exists for ${currentDate}. Catchup skipped.`);
124
145
}
125
125
-
if (!stationIds.length) stationIds = [String(stationSetting)];
146
146
+
} catch (e) {
147
147
+
console.error("[forecast] Catchup check failed:", e);
148
148
+
}
149
149
+
}
150
150
+
}, 3600000); // Check every hour (3600000 ms)
126
151
127
127
-
console.log(`[forecast] Processing ${stationIds.length} station(s)...`);
152
152
+
console.log(`[forecast] Poller active: checking every hour`);
128
153
129
129
-
for (const sid of stationIds) {
130
130
-
try {
131
131
-
console.log(`[forecast] → Station ${sid}: Storing forecasts...`);
132
132
-
await storeForecastForStation(sid);
133
133
-
134
134
-
console.log(`[forecast] → Station ${sid}: Calculating analysis...`);
135
135
-
await calculateAndStoreDailyAnalysis(sid);
136
136
-
console.log(`[forecast] → Station ${sid}: Backfilling analysis for last 7 days...`);
137
137
-
await backfillForecastAnalysis(sid, 7);
154
154
+
// Trigger an immediate check on startup (after 10s delay to let DB settle)
155
155
+
setTimeout(async () => {
156
156
+
console.log("[forecast] Running startup check...");
157
157
+
try {
158
158
+
const { getDuckConn } = await import("@/lib/db/duckdb");
159
159
+
const conn = await getDuckConn();
160
160
+
const now = new Date();
161
161
+
const currentDate = now.toISOString().split('T')[0];
138
162
139
139
-
console.log(`[forecast] ✓ Station ${sid}: Complete`);
140
140
-
} catch (e: any) {
141
141
-
console.error(`[forecast] ✗ Station ${sid} failed:`, e?.message || e);
142
142
-
}
163
163
+
const checkQuery = `SELECT 1 FROM forecasts WHERE storage_date = '${currentDate}' LIMIT 1`;
164
164
+
const result = await conn.runAndReadAll(checkQuery);
165
165
+
const hasData = result.getRowObjects().length > 0;
143
166
144
144
-
// Small delay to be gentle on upstream APIs
145
145
-
await new Promise(r => setTimeout(r, 250));
146
146
-
}
147
147
-
148
148
-
console.log(`[forecast] ========================================`);
149
149
-
console.log(`[forecast] DAILY POLLER COMPLETE`);
150
150
-
console.log(`[forecast] ========================================`);
151
151
-
} catch (e: any) {
152
152
-
console.error("[forecast] Daily storage failed:", e?.message || e);
167
167
+
if (!hasData) {
168
168
+
console.log(`[forecast] Startup catchup triggered for ${currentDate}`);
169
169
+
lastCatchupRunDate = currentDate;
170
170
+
await runForecastJob(stationSetting);
171
171
+
} else {
172
172
+
console.log(`[forecast] Startup check: Data exists for ${currentDate}`);
173
173
+
lastCatchupRunDate = currentDate;
153
174
}
175
175
+
} catch (e) {
176
176
+
console.error("[forecast] Startup check failed:", e);
154
177
}
155
155
-
}, 600000); // Check every 10 minutes (600000 ms)
156
156
-
157
157
-
console.log(`[forecast] Poller active: checking every 10 minutes for 20:00 window (20:00-20:30)`);
178
178
+
}, 10000);
179
179
+
}
180
180
+
}
181
181
+
182
182
+
async function runForecastJob(stationSetting: string) {
183
183
+
try {
184
184
+
// Resolve station list
185
185
+
let stationIds: string[] = [];
186
186
+
if (stationSetting === 'ALL') {
187
187
+
try {
188
188
+
// Fetch station list directly from Geosphere API
189
189
+
const res = await fetch('https://dataset.api.hub.geosphere.at/v1/station/current/tawes-v1-10min/metadata');
190
190
+
if (res.ok) {
191
191
+
const data = await res.json();
192
192
+
stationIds = (data.stations || []).map((s: any) => String(s.id));
193
193
+
}
194
194
+
} catch (e) {
195
195
+
console.error('[forecast] Failed to load station list for ALL:', e);
196
196
+
}
197
197
+
}
198
198
+
if (!stationIds.length) stationIds = [String(stationSetting)];
199
199
+
200
200
+
console.log(`[forecast] Processing ${stationIds.length} station(s)...`);
201
201
+
202
202
+
for (const sid of stationIds) {
203
203
+
try {
204
204
+
console.log(`[forecast] → Station ${sid}: Storing forecasts...`);
205
205
+
await storeForecastForStation(sid);
206
206
+
207
207
+
console.log(`[forecast] → Station ${sid}: Calculating analysis...`);
208
208
+
await calculateAndStoreDailyAnalysis(sid);
209
209
+
console.log(`[forecast] → Station ${sid}: Backfilling analysis for last 7 days...`);
210
210
+
await backfillForecastAnalysis(sid, 7);
211
211
+
212
212
+
console.log(`[forecast] ✓ Station ${sid}: Complete`);
213
213
+
} catch (e: any) {
214
214
+
console.error(`[forecast] ✗ Station ${sid} failed:`, e?.message || e);
215
215
+
}
216
216
+
217
217
+
// Small delay to be gentle on upstream APIs
218
218
+
await new Promise(r => setTimeout(r, 250));
219
219
+
}
220
220
+
221
221
+
console.log(`[forecast] ========================================`);
222
222
+
console.log(`[forecast] JOB COMPLETE`);
223
223
+
console.log(`[forecast] ========================================`);
224
224
+
} catch (e: any) {
225
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
168
-
236
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
174
-
242
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
196
-
264
264
+
197
265
if (!stationsResponse.ok) {
198
266
throw new Error(`Failed to fetch station metadata: ${stationsResponse.status} ${stationsResponse.statusText}`);
199
267
}
200
200
-
268
268
+
201
269
const stationsData = await stationsResponse.json();
202
270
const station = stationsData.stations.find((s: any) => s.id === stationId);
203
203
-
271
271
+
204
272
if (!station) {
205
273
throw new Error(`Station ${stationId} not found in metadata`);
206
274
}
207
207
-
275
275
+
208
276
console.log(`[forecast-store] ✓ Station found: ${station.name} (${station.lat}, ${station.lon})`);
209
209
-
210
210
-
277
277
+
278
278
+
211
279
const lat = station.lat;
212
280
const lon = station.lon;
213
213
-
281
281
+
214
282
// Fetch forecasts from all 4 sources - DIRECTLY from external APIs
215
283
const sources = ['geosphere', 'openweather', 'meteoblue', 'openmeteo'];
216
216
-
284
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
221
-
289
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
235
-
303
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
245
-
313
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
266
-
334
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
270
-
338
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
283
-
351
351
+
284
352
} else if (sourceName === 'openweather') {
285
353
const apiKey = process.env.OPENWEATHER_API_KEY;
286
354
if (!apiKey) continue;
287
287
-
355
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
291
-
359
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
300
-
368
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
308
-
376
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
318
-
386
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
330
-
398
398
+
331
399
} else if (sourceName === 'meteoblue') {
332
400
const apiKey = process.env.METEOBLUE_API_KEY;
333
401
if (!apiKey) continue;
334
334
-
402
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
338
-
406
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
347
-
415
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
360
-
428
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
372
-
440
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
377
-
445
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
386
-
454
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
399
-
467
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
416
-
484
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
435
-
503
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
448
-
516
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
467
-
535
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
472
-
540
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
496
-
564
564
+
497
565
console.log(`[forecast-analysis] ✓ Analysis table created/verified`);
498
498
-
566
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
507
-
575
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
536
-
604
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
556
-
624
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
566
-
634
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
638
-
706
706
+
639
707
console.log(`[forecast-analysis] Querying forecasts from DB...`);
640
708
console.log(`[forecast-analysis] Query:`, forecastQuery.trim());
641
641
-
709
709
+
642
710
const forecastReader = await conn.runAndReadAll(forecastQuery);
643
711
const forecasts: any = forecastReader.getRowObjects();
644
644
-
712
712
+
645
713
console.log(`[forecast-analysis] Found ${forecasts.length} forecast rows for YESTERDAY (${yesterdayStr})`);
646
646
-
714
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
652
-
720
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
660
-
728
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
663
-
731
731
+
664
732
// Store analysis for each source once
665
733
let stored = 0;
666
734
for (const forecast of Object.values(latestBySource)) {
667
667
-
const tempMinError = actualConverted.tempMin !== null && forecast.temp_min !== null
735
735
+
const tempMinError = actualConverted.tempMin !== null && forecast.temp_min !== null
668
736
? Math.abs(actualConverted.tempMin - forecast.temp_min) : null;
669
669
-
const tempMaxError = actualConverted.tempMax !== null && forecast.temp_max !== null
737
737
+
const tempMaxError = actualConverted.tempMax !== null && forecast.temp_max !== null
670
738
? Math.abs(actualConverted.tempMax - forecast.temp_max) : null;
671
671
-
const precipitationError = actualConverted.precipitation !== null && forecast.precipitation !== null
739
739
+
const precipitationError = actualConverted.precipitation !== null && forecast.precipitation !== null
672
740
? Math.abs(actualConverted.precipitation - forecast.precipitation) : null;
673
673
-
const windSpeedError = actualConverted.windSpeed !== null && forecast.wind_speed !== null
741
741
+
const windSpeedError = actualConverted.windSpeed !== null && forecast.wind_speed !== null
674
742
? Math.abs(actualConverted.windSpeed - forecast.wind_speed) : null;
675
675
-
743
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
678
-
746
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
706
-
774
774
+
707
775
stored++;
708
776
}
709
709
-
777
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`);