feat: T4b/T4c wave 8 - weather history, phenology, honey chain, Telegram base
- Weather history from Open-Meteo behind an adapter: worker loop phenology-weather with backoff, Retry-After, User-Agent, chunked writes and forecast-to-archive correction - Phenology: growing degree sum per DWD, phase from reports, GTS and calendar, calibration from previous years, daily job step, SeasonService reads the stored state - Honey chain: prefill for extractions and batches (proposals only), plausibility and withdrawal warnings, Extraction lock, chain events and batch.created webhook from HoneyBatchRecorded - Telegram base: schema, NotificationDelivery outbox, hardened bot client, startup check without crash, no-network-outside-loops guard - ARCHITECTURE.md: sections 11.11-11.13, assumptions 42-46, env table Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
22382d53a2
commit
1971b2d9c4
90 files changed
+8431
-155
No files matched your search
@@ -4,8 +4,12 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
WeatherHistoryConfig,
|
||||
weatherHistoryConfig,
|
||||
roundPoint,
|
||||
} from './history/weather-history.config';
|
||||
|
||||
const OPEN_METEO_FORECAST_URL = 'https://api.open-meteo.com/v1/forecast';
|
||||
const PAST_DAYS = 14;
|
||||
const FORECAST_DAYS = 7;
|
||||
const RECENT_WINDOW_DAYS = 7;
|
||||
@@ -66,6 +70,10 @@ interface OpenMeteoResponse {
|
||||
|
||||
@Injectable()
|
||||
export class WeatherService {
|
||||
// Same variables as the history source (one place for URL and key); a bad
|
||||
// setting stops the process at startup.
|
||||
private readonly config: WeatherHistoryConfig = weatherHistoryConfig();
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getWeather(apiaryId: string): Promise<ApiaryWeather> {
|
||||
@@ -107,9 +115,11 @@ export class WeatherService {
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
): Promise<DailyWeather[]> {
|
||||
const url = new URL(OPEN_METEO_FORECAST_URL);
|
||||
url.searchParams.set('latitude', String(latitude));
|
||||
url.searchParams.set('longitude', String(longitude));
|
||||
const url = new URL(this.config.forecastUrl);
|
||||
// 0.01 degrees: no exact stand location leaves the app.
|
||||
const point = roundPoint(latitude, longitude);
|
||||
url.searchParams.set('latitude', String(point.lat));
|
||||
url.searchParams.set('longitude', String(point.lon));
|
||||
url.searchParams.set(
|
||||
'daily',
|
||||
'temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,weather_code',
|
||||
@@ -118,11 +128,15 @@ export class WeatherService {
|
||||
url.searchParams.set('past_days', String(PAST_DAYS));
|
||||
url.searchParams.set('forecast_days', String(FORECAST_DAYS));
|
||||
url.searchParams.set('timezone', 'auto');
|
||||
if (this.config.apiKey) {
|
||||
url.searchParams.set('apikey', this.config.apiKey);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
headers: { 'User-Agent': this.config.userAgent },
|
||||
});
|
||||
} catch {
|
||||
throw new BadRequestException('Wetterdaten konnten nicht geladen werden');
|
||||
|
||||
Reference in new issue
Block a user