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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 20:00:46 +02:00
1 parent 22382d53a2
commit 1971b2d9c4
90 files changed
+8431 -155

No files matched your search

@@ -321,4 +321,43 @@ describe('assertStartupEnvironment', () => {
).not.toThrow();
});
});
describe('telegram bot token', () => {
const TOKEN = '123456789:AAEhBOweik6ad9r_QXMENQjcrGbqCr4K-RM';
it('starts without a token in every role (Telegram stays disabled)', () => {
for (const role of ['api', 'worker', 'job', 'all'] as const) {
expect(() => assertStartupEnvironment(role, PROD_ENV)).not.toThrow();
}
});
it('the worker accepts a well-formed token in production', () => {
expect(() =>
assertStartupEnvironment('worker', {
...PROD_ENV,
TELEGRAM_BOT_TOKEN: TOKEN,
}),
).not.toThrow();
});
it('a malformed or placeholder token never stops the worker (Telegram stays off)', () => {
for (const bad of ['garbage', 'change-me-telegram-token']) {
expect(() =>
assertStartupEnvironment('worker', {
...PROD_ENV,
TELEGRAM_BOT_TOKEN: bad,
}),
).not.toThrow();
}
});
it('the API pod does not look at the token', () => {
expect(() =>
assertStartupEnvironment('api', {
...PROD_ENV,
TELEGRAM_BOT_TOKEN: 'garbage',
}),
).not.toThrow();
});
});
});
@@ -3,6 +3,7 @@ import {
PREVIOUS_KEY_SUFFIX,
readRingSecrets,
} from '../common/crypto/key-ring';
import { assertTelegramToken } from '../telegram/telegram-config';
import type { ApicoreRole } from './role';
const REQUIRED_ENV_VARS = ['DATABASE_URL', 'JWT_ACCESS_SECRET'] as const;
@@ -130,16 +131,17 @@ function assertEncryptionKeys(env: NodeJS.ProcessEnv): void {
/**
* Startup checks shared by every entry point (API, worker, CLI job). All
* roles load the same modules and secrets today, so the checks do not differ
* per role yet; the role is part of the signature so role-specific checks can
* be added without touching the callers.
* roles load the same modules and secrets today, so the checks hardly differ
* per role; the Telegram bot token is the first role-specific one (only the
* worker holds it).
*/
export function assertStartupEnvironment(
_role: ApicoreRole,
role: ApicoreRole,
env: NodeJS.ProcessEnv = process.env,
): void {
assertRequiredEnvVars(env);
assertProductionSettings(env);
assertNoPlaceholderSecrets(env);
assertEncryptionKeys(env);
assertTelegramToken(role, env);
}