Brings the ERP inventory export up to date with the platform backbone and
field capture work. Conflicts in ARCHITECTURE.md, the API key service and
the API key settings tab resolved by keeping both sides:
- read and write keys stay separate; erp-inventory:read is read-only and
cannot be combined with a write scope
- the failed-attempt lockout counts in the shared ThrottlerHit table so it
holds across API pods
- the UI offers only scopes the user holds and creates one scope per key
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks:
- run roles api/worker/job/all with shared startup checks (Task 1)
- schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2)
- audit, notifications and domain events written in the same transaction (Task 3)
- dispatcher with SKIP LOCKED claims, retries and stale release (Task 4)
- worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5)
- worker process and entry points (Task 6)
- throttler counters in Postgres (Task 19)
- separate keys per purpose with key ring (Task 20)
- public origin page without exact location (Task 22)
- shop keys split into read and write (Task 23)
- Kustomize base, components, overlay encry, CI validation (Task 26)
Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks:
- brood frames, read endpoints, apiary overview and offline pack
- network basics, offline queue v3, navigation sheets, form building blocks
- new inspection with last-time hints and weather, varroa per day,
treatment prefill, apiary page with round
- inspection photo (Task 10) is implemented, review pending
Not rolled out. Remaining tasks follow in later commits.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Befunde aus dem Sicherheitsreview des Branches:
- MEDIUM: Die Sperre lief vor der Schlüsselsuche und zählte auch falsch
aufgebaute Tokens. Wer die gemeinsame Adresse (Cluster-Ausgang, Proxy)
nutzt oder per X-Forwarded-For vortäuscht, hätte ERPNext und den Shop
15 Minuten aussperren können. Jetzt: erst Suche über den Hash; ein
gültiger, nicht widerrufener, nicht abgelaufener Schlüssel kommt immer
durch; 429 nur für unbekannte, widerrufene und abgelaufene Schlüssel;
falsch aufgebaute Tokens sofort 401 ohne Zählung. Beim Start warnt
ApiCore, wenn TRUST_PROXY in Produktion fehlt (im Cluster gesetzt).
- LOW: Bei vollem Speicher verdrängt der Limiter zuerst nicht gesperrte
Einträge, gesperrte erst zuletzt.
- LOW: Bewegungs-Cursor mit Jahr außerhalb 2000-2100 ergab 500, jetzt 400.
- LOW/INFO: Vertrag legt fest, dass ERPNext Produkte und Chargen bei jedem
Lauf vollständig neu liest (Voll-Spiegel) und Bewegungen inkrementell;
remainingQuantity ist live, Bewegungen kommen 5 Minuten verzögert.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Wie der Throttler seit 886b4e7: die Sperre nach abgelehnten Schlüsseln
zählt über rateLimitSubject, damit wechselnde Adressen im eigenen
IPv6-/64 das Budget nicht zurücksetzen. Im Log steht weiter die Adresse.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Datenquelle für den nächtlichen Inventar-Abgleich in ERPNext (ERPNext-
Ausbau T8, Entscheidungen E2/E3). Neues Recht erp-inventory:read, auch als
API-Schlüssel-Scope; es öffnet nur diese vier GET-Endpunkte:
- /api/v1/export/inventar/produkte: Artikel (ohne Beschreibung)
- .../chargen: Abfüllungen und Herstellungschargen mit Losnummer, MHD,
Ausgangsmenge, Rest aus dem Bewegungsbuch, Füllmenge g und Sorte
- .../bewegungen?seit=&nach=&limit=: Shop-Verkauf, Retoure, Entnahmen mit
Grund (HOFLADEN = Vertrauensschrank, EIGENBEDARF, BRUCH, PROBE,
SONSTIGES) und Inventur; Cursor über (createdAt, id), Bewegungen erst
nach 5 Minuten Wartezeit, damit der Cursor keine offene Buchung überholt
- .../ernten?jahr=: Nettogewicht kg je Kalenderjahr, wie die Statistik
Alle Listen stabil sortiert, seitenweise (1-500), Parameter mit
class-validator geprüft, Cursor je Endpunkt streng validiert. Feld-
Allowlists: keine Notizen, Benutzer, Standorte, Rohstoffe. Die Gründe gab
es im Datenmodell schon - keine Migration; das Recht legt der Katalog-
Sync beim Start an und gibt es der Rolle Administrator.
Vertrag mit Feldern, Einheiten, Fehlercodes und Beispielen:
docs/erp-inventar-export.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Nach 20 abgelehnten API-Schlüsseln (fehlerhaft, unbekannt, widerrufen,
abgelaufen) in 15 Minuten antwortet ApiCore der Absender-IP mit 429, auch
für richtige Schlüssel - vor jeder Datenbankabfrage. Gleiches Budget wie
der ERP-Export des Shops. Die Zählung liegt wie der Throttler im Speicher
des Pods und ist auf 10.000 Adressen begrenzt.
Abgelehnte Schlüssel werden mit Grund, öffentlichem Präfix (ack_xxxxxxxx)
und IP protokolliert, nie mit dem Schlüssel. Die Suche bleibt über den
SHA-256-Hash; neu sind Unit-Tests für authenticate (fehlend, unbekannt,
widerrufen, abgelaufen, Scope-Filter, IP-Allowlist, Sperre).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
One remaining quantity per batch, kept in ApiCore and always derived:
initial quantity (jarCount / produced quantity) plus the signed movements
in the new BatchMovement ledger, which is never deleted.
- Production batches for MANUFACTURED articles (candles, propolis
products, ...); articles get a kind and a shop category.
- Withdrawals (farm shop, own use, breakage, sample) and inventory
corrections on bottlings and production batches, never more than the
remaining stock; every write locks the bottling or the article row.
- Shop bookings via POST /shop-sync/movements (new scope
shop-sync:write): idempotent per externalId, articles allocated oldest
best-before first, returns mirrored onto the batches of the sale,
invalid entries rejected one by one.
- GET /shop-sync/stock and remainingCount in the shop feed; webhook
stock.changed after every change (ids only, receivers fetch numbers).
- Bottlings and production batches with movements cannot be deleted.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A key can be limited to IP addresses or CIDR networks (e.g. the Wabenhain
server); a stolen key is useless from anywhere else. Requests from other
addresses get 403 "not allowed for this IP" and a log line. The admin UI
asks for the addresses when creating a key, warns when a key works from
anywhere, shows the last source address and can take it over into the
allowlist - changing the list needs no new key (PATCH /api-keys/:id).
Also: a key can only carry scopes its creator holds.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Integrations such as the Wabenhain shop sync no longer need a service
account with a password (and without MFA). An admin creates a key under
Verwaltung > Automatisierung > API-Schlüssel.
- Key format ack_<8>_<43>; only its SHA-256 hash is stored, the key is
shown exactly once. Optional expiry, "last used" timestamp, revocation
takes effect immediately; create/revoke are audit-logged without the key.
- A key is not a user: JwtAuthGuard sets request.apiKey, PermissionsGuard
admits it only to routes whose permission is among its scopes and never
to user-only routes. Scopes come from a short allowlist (shop-sync:read).
- Managing keys requires the new permission api-keys:manage.
- Keys are excluded from backups on purpose; a restore means issuing a new
key.
Additive migration (new table).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>