feat: T4b/T4c wave 9 - phenology reports, autopilot A4-A10, honey ledger, Telegram delivery
Backend Image / build-and-push (push) Successful in 4m26s
Frontend Image / build-and-push (push) Successful in 7m19s
Manifests / validate (push) Successful in 17s

- Phenology reports API with corrections and revokes; recompute per
  apiary via PhenologyRecomputeRequested, allowBackward for corrections;
  season changes are announced in the morning digest (changes since the
  previous digest, each once)
- Season autopilot A4-A10 with per-apiary sync (Apiary before Colony
  locks) and WinterCheckRecorded
- Honey batch ledger at runtime: initial amount, BOTTLING bookings,
  manual movements, one gram rounding, untracked bottlings never book
- Telegram delivery: planning, Berlin delivery times incl. DST, quiet
  hours, permission re-check and stale skip at send time, rate limits,
  cleanup of old deliveries
- ARCHITECTURE.md: assumptions 47-50 and updated sections 11.x

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 21:13:23 +02:00
1 parent 1971b2d9c4
commit 16cc557462
74 files changed
+9634 -340

No files matched your search

@@ -0,0 +1,105 @@
import {
renderTelegramText,
TELEGRAM_TEMPLATES,
TelegramTemplateCode,
} from './telegram-texts';
const ORIGIN = 'https://app.example.org';
describe('TELEGRAM_TEMPLATES', () => {
it('has the exact texts of the bot replies', () => {
expect(TELEGRAM_TEMPLATES).toEqual({
LINKED:
'Verbunden mit ApiCore. Hier kommen deine Erinnerungen. /stop beendet das.',
LINK_INVALID:
'Der Link ist ungültig oder abgelaufen. Bitte in ApiCore neu verbinden.',
STOPPED: 'Abgemeldet. Du bekommst keine Nachrichten mehr.',
HELP: 'Dieser Bot schickt Erinnerungen aus ApiCore. Verbinden geht nur in ApiCore unter Profil > Benachrichtigungen. /stop beendet.',
} satisfies Record<TelegramTemplateCode, string>);
});
});
describe('renderTelegramText', () => {
it('sends the message, a blank line and the app link', () => {
const text = renderTelegramText(
{ message: 'Heute fällig: Futter bestellen', link: '/tasks' },
true,
ORIGIN,
);
expect(text).toBe(
'Heute fällig: Futter bestellen\n\nhttps://app.example.org/tasks',
);
});
it('falls back to /dashboard without a link', () => {
const text = renderTelegramText({ message: 'm', link: null }, true, ORIGIN);
expect(text).toBe('m\n\nhttps://app.example.org/dashboard');
});
it('without details only says there is a new message, plus the link', () => {
const text = renderTelegramText(
{ message: 'Heute fällig: Schwarmzelle Völker', link: '/tasks' },
false,
ORIGIN,
);
expect(text).toBe(
'Neue Meldung in ApiCore.\n\nhttps://app.example.org/tasks',
);
expect(text).not.toContain('Schwarm');
});
it('tolerates a trailing slash of the origin', () => {
const text = renderTelegramText(
{ message: 'm', link: '/tasks' },
true,
`${ORIGIN}/`,
);
expect(text.endsWith('https://app.example.org/tasks')).toBe(true);
});
it('cuts a long message to at most 600 characters and keeps the link whole', () => {
const text = renderTelegramText(
{ message: 'ä'.repeat(2000), link: '/tasks' },
true,
ORIGIN,
);
expect([...text].length).toBeLessThanOrEqual(600);
expect(text.endsWith('\n\nhttps://app.example.org/tasks')).toBe(true);
expect(text).toContain('…\n\n');
});
it('counts code points, not UTF-16 units', () => {
const text = renderTelegramText(
{ message: '🐝'.repeat(1000), link: '/tasks' },
true,
ORIGIN,
);
expect([...text].length).toBeLessThanOrEqual(600);
expect(text).not.toMatch(/[\ud800-\udbff](?![\udc00-\udfff])/);
});
it('removes control characters and invisible characters from the message', () => {
const text = renderTelegramText(
{ message: 'a\u0007b‮c​d\r\ne', link: '/tasks' },
true,
ORIGIN,
);
expect(text).toBe('abcd\ne\n\nhttps://app.example.org/tasks');
});
it('without an origin sends the text alone, no link', () => {
expect(
renderTelegramText({ message: 'Hallo', link: '/tasks' }, true, null),
).toBe('Hallo');
expect(
renderTelegramText({ message: 'Hallo', link: '/tasks' }, false, null),
).toBe('Neue Meldung in ApiCore.');
});
});