feat: T4 wave 10 – bottling quality, phenology and honey chain UI, Telegram link/receive and ops

- Bottling quality: water content above the legal limit (strict >) answers
  409 CONFIRMATION_REQUIRED bound to the measured value; prefill for jar
  size, best-before (shelfLifeMonths, month-end clamping) and water content;
  impossible dates give 400; water-content checks in src/common/quality.
- Frontend phenology: "Entwicklung melden" sheet, IndexedDB v4 queue,
  season header on the apiary page, confirmed revoke for all apiaries.
- Frontend honey chain: "Vorschlag aus …" hints with explicit take-over,
  prefill status note, movement ledger with withdraw/correct dialogs.
- Telegram: link tokens (hashed, single use), private-chat receiver with
  lease and offset CAS, bounded poison-update skip, Retry-After, pending
  deliveries skipped on disconnect; system API for deliveries and retry;
  worker-only secret wiring and manifest policy tests.
- ARCHITECTURE.md §7, §9, §10, §11.11–11.13, Annahmen 51–55.

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 22:47:17 +02:00
1 parent 16cc557462
commit fab7f7e12f
91 files changed
+8463 -104

No files matched your search

+84
View File
@@ -315,6 +315,90 @@ test('resources match the spec table; HPA api 1–2 at 70 % CPU', () => {
assert.equal(api.doc.spec.replicas, undefined, 'the HPA owns the api replica count');
});
// The Telegram bot token belongs to the worker alone (Spec T4c): one optional
// secretKeyRef on its main container, no envFrom, no volume, no other pod.
const TELEGRAM_SECRET = 'apicore-telegram';
const TELEGRAM_TOKEN_KEY = 'TELEGRAM_BOT_TOKEN';
/** Every place a pod template can pull `secretName` in: env, envFrom, volumes. */
function secretReferences(pod, secretName) {
const refs = [];
const containers = [
...pod.containers.map((c) => ({ c, init: false })),
...pod.initContainers.map((c) => ({ c, init: true })),
];
for (const { c, init } of containers) {
for (const e of c.env ?? []) {
const ref = e.valueFrom?.secretKeyRef;
if (ref?.name === secretName) refs.push({ kind: 'env', container: c.name, init, name: e.name, ref });
}
for (const from of c.envFrom ?? []) {
if (from.secretRef?.name === secretName) refs.push({ kind: 'envFrom', container: c.name, init });
}
}
for (const v of pod.spec.volumes ?? []) {
const projected = (v.projected?.sources ?? []).some((src) => src.secret?.name === secretName);
if (v.secret?.secretName === secretName || projected) refs.push({ kind: 'volume', volume: v.name });
}
return refs;
}
for (const [label, model] of [['target', target], ...stages]) {
test(`${label}: only the worker references the apicore-telegram secret, and only optionally`, () => {
const workers = model.byApp('apicore-worker');
if (label === 'target' || workers.length > 0) {
assert.equal(workers.length, 1, `${label}: expected exactly one apicore-worker pod`);
}
for (const pod of model.pods) {
const refs = secretReferences(pod, TELEGRAM_SECRET);
if (pod.labels.app !== 'apicore-worker') {
assert.deepEqual(refs, [], `${pod.owner} must not use secret ${TELEGRAM_SECRET}`);
continue;
}
assert.equal(refs.length, 1, `${pod.owner}: expected exactly one reference to ${TELEGRAM_SECRET}`);
const [only] = refs;
assert.equal(only.kind, 'env', `${pod.owner}: the secret must come in as a single env variable`);
assert.equal(only.init, false, `${pod.owner}: init containers must not see the bot token`);
assert.equal(only.container, mainContainer(pod).name, `${pod.owner}: only the main container takes the token`);
assert.equal(only.name, TELEGRAM_TOKEN_KEY);
assert.deepEqual(only.ref, { name: TELEGRAM_SECRET, key: TELEGRAM_TOKEN_KEY, optional: true });
const tokenEnvs = [...pod.containers, ...pod.initContainers].flatMap((c) =>
(c.env ?? []).filter((e) => e.name === TELEGRAM_TOKEN_KEY),
);
assert.equal(tokenEnvs.length, 1, `${pod.owner}: expected exactly one ${TELEGRAM_TOKEN_KEY} entry`);
assert.equal(tokenEnvs[0].value, undefined, `${pod.owner}: ${TELEGRAM_TOKEN_KEY} must not be a literal`);
}
// No other pod may carry the token by value or under that variable name.
for (const pod of model.pods.filter((p) => p.labels.app !== 'apicore-worker')) {
for (const c of [...pod.containers, ...pod.initContainers]) {
assert.ok(
!(c.env ?? []).some((e) => e.name === TELEGRAM_TOKEN_KEY),
`${pod.owner}/${c.name} defines ${TELEGRAM_TOKEN_KEY}`,
);
}
}
});
test(`${label}: the bot name sits in the shared ConfigMap, the token never does`, () => {
const configMaps = model.docs.filter((d) => d.kind === 'ConfigMap' && d.metadata.name === 'apicore-backend-config');
assert.equal(configMaps.length, 1, 'expected one apicore-backend-config');
const data = configMaps[0].data ?? {};
assert.ok('TELEGRAM_BOT_USERNAME' in data, 'TELEGRAM_BOT_USERNAME missing in apicore-backend-config');
assert.ok(!(TELEGRAM_TOKEN_KEY in data), `${TELEGRAM_TOKEN_KEY} must not be in a ConfigMap`);
for (const doc of model.docs.filter((d) => d.kind === 'ConfigMap')) {
assert.ok(!(TELEGRAM_TOKEN_KEY in (doc.data ?? {})), `${doc.metadata.name} holds ${TELEGRAM_TOKEN_KEY}`);
}
// Both roles read the name from the one ConfigMap through envFrom.
// (The worker only exists from the stage that rolls out backend-roles.)
for (const pod of [...model.byApp('apicore-backend'), ...model.byApp('apicore-worker')]) {
assert.ok(
(mainContainer(pod).envFrom ?? []).some((f) => f.configMapRef?.name === 'apicore-backend-config'),
`${pod.owner} does not read apicore-backend-config`,
);
}
});
}
test('api and worker run prisma migrate deploy as initContainer; only api seeds', () => {
const api = onePod(target, 'apicore-backend');
const worker = onePod(target, 'apicore-worker');