- Withdrawal wording from one backend helper everywhere ("bis
einschließlich TT.MM. – ab TT.MM. wieder erntefähig", year when it
crosses New Year); withdrawal-status returns lastBlockedDay and the
ready text, the frontend no longer computes dates
- Calendar events name colony or apiary, withdrawal events carry
product and detail; compact withdrawal rows on mobile
- One bottling warning per colony and treatment; pre-save hint
- Offline: online-only actions disabled with a reason, cached data
shows its timestamp, open sheets block submit offline
- Mobile: no horizontal scroll at 360 px, touch targets >= 44 px,
toasts and the add button no longer cover content
- Cleanup sheet without destructive preselection; natural sort of
colony codes; collapsible automation rule cards; varroa list newest
first with chart axes; text fixes
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- In-app reminders: task due (skips rescheduled tasks, catch-up wording)
and a per-user morning digest step in the daily job
- Frontend: "Als Nächstes" on the colony page, "Heute" on the dashboard,
inspection-due card, season chip, apiary overview fields, cleanup
sheet for ended colonies
- Frontend: plausibility warnings that stay until dismissed, withdrawal
spans in the calendar, notification bell with categories and links,
notification settings in the profile
- ARCHITECTURE.md: T4a sections 11.1-11.10, assumptions 31-41, handler
table and daily job step order
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Colony end releases the queen in the same transaction; open
suggestions become obsolete and a CLEANUP suggestion lists open
tasks and the queen; apply-cleanup validates evidence and blocks
reactivated colonies
- Withdrawal chain: plausibility warnings on harvest and bottling,
conflict notifications also when a treatment is entered or corrected
after the harvest (naming bottled lots), withdrawal spans in the
calendar and M365 sync, Berlin-day end events
- Next 3 steps per colony and the "Heute" endpoint with ranking and an
explicit merge table by intent
- Inspection-triggered rule suggestions follow the colony's latest
inspection (resolve when the finding is gone, refresh when it stays)
- Frontend: colony card takeover in the inspection form, feed status as
condition, occupied frames 0-40; legacy offline entries keep syncing
- Notifications and preferences are deleted with their user
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Automation rules propose suggestions instead of creating tasks;
notify per rule config; corrected varroa measurements refresh or
resolve their suggestion
- Inspection values can be taken over into the colony card
(occupiedFrames 0-40, strength deprecated); warning counts use the
same visibility rule as the notification list
- Biotech chains: swarm check after queen cells, queen acceptance
check, split schedules (raise own, queen cell, queen)
- Season autopilot core with A1 inspection due, A2 varroa measurement,
A3 recheck after treatment; colony sync and daily step
- Frontend: suggestions page with groups, bulk accept, undo, paging,
focus management and permission gating
- Frontend: varroa traffic light from seasonal bands, sample size,
one range request for the colony chart
- Notifications: mark-read answers 404 for rows the user may no
longer see
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Schema III: per-user notifications (broadcast fan-out migration,
category, link, requiredPermissions filter), notification preferences
and settings endpoints, calendar withdrawal switch, occupiedFrames
- Suggestions API: list/summary with permission visibility and paging,
accept/dismiss/reopen incl. bulk, SuggestionView on all responses
- Varroa traffic light by season and method, young colonies, treatment
effect window, sample-based percentage; shared withdrawal-window helper
on Berlin calendar days; GET /varroa-measurements/thresholds
- ColonySplit and QueenAssigned domain events with calendar and webhook
handlers
- Outbox claims compare against now() rounded to timestamp(3); fixes the
flaky e2e claims of rows written with SQL now()
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Rule configs with typed parsers and registry; Berlin day helpers
- Schema: T4 enums, Suggestion table, feed vocabulary (feedStatus as
ColonyCondition with legacy text mapping), split queen source,
varroa sample size; migration harness for data migrations
- SuggestionsService: propose/refresh/resolve/obsolete/expire,
accept/dismiss/reopen incl. bulk, lock order, concurrency e2e
- SeasonService: phenology-based segments, inspection/varroa due dates,
queen age; TimeEventsStep with JobPartialFailure
- Frontend: per-rule settings forms with visible value sources
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Brings the ERPNext inventory export (T8) into main after its review:
strict seit format, settle window on the DB clock, BatchMovement
(createdAt, id) index, one scope per new API key and the failed-attempt
lockout on the shared throttler storage. Conflicts resolved in the
permission catalog (System and ERP-Anbindung groups) and ARCHITECTURE.md
(lockout is Annahme 30).
Gates on the merged state: backend lint, typecheck, 1909 unit, 471 e2e,
build, no schema drift; frontend 929 tests, lint, typecheck, build.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed:
- webhooks with delivery table, retries and stable delivery id
- calendar handlers; a finished task removes its Outlook event
- tasks, automations, inspections, varroa, treatments, feedings,
harvests, colonies, wintering, stock and bottling record domain events
in the same transaction; legacy post-commit calls removed
- row locks through one helper (lockRowForUpdate, lockRowsForShare,
lockRowsForUpdate)
- job framework with JobRun, cli exit codes, cleanup, task templates,
daily evaluation, hourly app-backup and the rekey job
- system API and page "Verwaltung > System" with retry
- @nestjs/schedule removed, guard tests against in-process schedulers
Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed:
- inspection photo offline without metadata
- bulk capture for treatments, feedings and varroa (backend + frontend)
- offline stand mode with service worker, offline pages and packs
- offline session, cold start and logout on shared devices
Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md.
Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema
drift; frontend lint, typecheck, 913 tests, build. Not rolled out.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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]>
Backend
- Backup covers every Prisma model (bundle v3); a guard test fails for any
new model that is neither backed up nor explicitly excluded. Unsealed
bundles may only add rows to 27 legacy master/field tables and never
overwrite; counters, ledgers, settings, roles etc. need a sealed file.
Shared import limit BACKUP_MAX_IMPORT_BYTES (default 50 MB, max 256 MB)
with warnings in log and UI; replaced document files are removed after
commit.
- Post-commit side effects (automation, calendar sync, webhooks) run via
runAfterCommit and never turn a committed write into a 500, so retries
with the same Idempotency-Key no longer create duplicates.
- Calendar outbox is multi-replica safe: per-entry claims with stale
takeover, revision-based settle, new Graph event only on 404/410,
transactionId on create, 30 s Graph timeout, URL-encoded ids, GUID
validation for groupId, "run now" in background and resets FAILED.
- Automation tasks are deduplicated under a row lock on the rule.
- Update DTOs follow one null rule: omitted = unchanged, null clears a
nullable field, null on a required field is a 400.
- Varroa mites/day is recomputed on correction; withdrawal status is
checked against the harvest date (?at=), bounded colonyIds; inspection
create is audited; updates re-enqueue calendar entries.
- Dashboard, statistics and apiary/colony guards treat WEAK, CONSPICUOUS
and QUEENLESS colonies as living.
- Prisma errors and body-parser errors are logged without query values or
request bodies.
- Migration 20261012100000_calendar_sync_claims; schema index name aligned
with the existing database object.
Frontend
- Sticky save bar sticks again (no scroll container ancestors, card uses
overflow-clip), safe-area insets, scroll-padding, FAB hidden on forms,
sticky desktop sidebar.
- Inspections record yes / no / not checked instead of silent defaults.
- All living colonies are selectable; shared RecordSelect, EnumSelect,
ChoiceGroup and TriStateField replace per-page copies; selects show
names instead of ids; labels point at real controls.
- Emptied optional fields are sent as null (emptyToNull) so every field
can be cleared; dates default to the local day and are sent via
toApiDate.
- Harvest withdrawal warning uses the harvest date; backup page warns when
a backup outgrows the import limit and lists skipped tables.
Build and docs
- Docker builds fail unless lint, typecheck and unit tests pass.
- ARCHITECTURE.md updated (assumptions 9-12, 19-21, section 8) and spec
docs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md
for the backbone (api/worker/job roles, domain event outbox) and the
feature roadmap T3-T7.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Der Server lehnt Rechte ab, die der Anlegende nicht selbst hat. Das UI
bietet jetzt auch nur diese an (z. B. kein shop-sync:write ohne eigenes
Recht) und erklärt, wenn gar keines übrig bleibt.
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]>
Bisher vergab die Oberfläche fest shop-sync:read. Jetzt sind alle
freigegebenen Rechte ankreuzbar (Shop lesen, Shop-Buchungen,
Inventar-Spiegel für ERPNext); vorausgewählt bleibt wie bisher nur
Shop lesen, ohne Haken lässt sich nichts anlegen. Die Liste zeigt, was
ein Schlüssel darf, und der Hinweis nach dem Anlegen nennt das Zielsystem.
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]>
Die cloudflare-only-Annotation ist eine Traefik-Annotation und war mit
ingressClassName nginx wirkungslos. Produktion laeuft auf RKE2 mit Traefik
(Ingresses apicore-backend/-frontend), die Vorlagen nennen jetzt dieselbe
Klasse.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Die live gesetzte Annotation traefik.ingress.kubernetes.io/router.middlewares
fehlte in den Manifesten (Backend und Frontend). Sie schuetzt vor
Direktzugriff am Origin vorbei, nicht vor fremden Cloudflare-Zonen.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Die Kommentare beschrieben noch einen einzelnen Reverse-Proxy-Hop. Sie
beschreiben jetzt die Cloudflare/Traefik-Liste. Beim Start wird einmal
auf info-Level geloggt, ob Hop-Zahl, Liste oder der Default aktiv ist
(nur Anzahl, keine Adressen), damit eine fehlende Produktionseinstellung
auffaellt. Das Standardverhalten bleibt unveraendert.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Der Standard-ThrottlerGuard trackt die volle req.ip. Ein IPv6-Client
kann innerhalb seines /64 beliebig Adressen rotieren und so die
Login-/Refresh-/SSO-Limits umgehen. Neuer ClientIpThrottlerGuard
liefert IPv4 unveraendert, IPv4-mapped als IPv4 und IPv6 als /64-Praefix.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
LAN-Adressen als vertrauenswuerdige Hops waren unnoetig weit: Traefik
verwirft X-Forwarded-For von LAN-Clients ohnehin, sie sind selbst Absender.
Vertraut werden nur noch 10.42.0.0/16 (Traefik-Pods) und die Cloudflare-Netze.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Mit TRUST_PROXY=1 (Standard) war req.ip die Cloudflare-Edge, weil Traefik
sie als letzten X-Forwarded-For-Eintrag anhaengt. Rate-Limits trafen damit
alle Clients derselben Edge gemeinsam, und IP-Freigaben von API-Schluesseln
sahen nie die echte Adresse.
Vorlage und .env.example nennen jetzt "loopback, uniquelocal" plus die
Cloudflare-Netze: Express nimmt den ersten Eintrag von rechts ausserhalb
dieser Netze, also den Client, den Cloudflare angehaengt hat. Vom Client
mitgeschickte Eintraege links davon erreicht die Auswertung nie.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Documents, avatars and the encrypted app backups lived in ./uploads and
./backups inside the container and were lost on every restart. The
backend now mounts the apicore-data volume at /data (configmap paths
/data/documents and /data/backups).
Both deployments pin the image to a CI build (sha-<commit>) with
imagePullPolicy IfNotPresent instead of :latest, run as uid 1000 with
seccomp RuntimeDefault, no privilege escalation and all capabilities
dropped. The frontend gets a liveness probe.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
CronJob apicore-db-backup dumps the database to the Storage Box
(StorageClass smb-backup, SMB 3.1.1 with seal). Keeps 14 daily and
8 weekly dumps.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Requests are set to an explicit 0 instead of being removed, because
Kubernetes defaults a missing request to the limit, which would reserve
even more. Limits stay as an upper bound.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The floating 18-alpine tag with IfNotPresent kept an old 18.x image
cached on the nodes, so minor releases never reached the cluster.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
With {ART} in the raw material number format (default {ART}-{YYYY}-{SEQ:4}),
beeswax and propolis each count on their own: WA-2026-0001, PR-2026-0001.
Counters live in the new AutomationSequence table (one row per rule and
scope). Without {ART} the rule keeps its single shared counter, since
separate counters behind one prefix would hand out duplicate numbers.
Resetting a rule clears all of its counters.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- RAW_MATERIAL_LOT_NUMBER_FORMAT / PRODUCTION_BATCH_NUMBER_FORMAT fill in the
lot number when it is left empty, like honey batches and bottlings.
- RAW_MATERIAL_LOW: task when the total of a raw material kind drops below
config.minGrams after consumption, withdrawal or inventory count.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Follow-up to a7609ff for the whole frontend: all 30 API helpers in
src/lib now pass path values through encodeURIComponent, so an id taken
from the URL can never leave its own path segment and make the page call
another endpoint with the viewer's token.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Security review finding (client-side path traversal): ids taken from the
URL were interpolated into API paths unencoded, so a crafted link like
/raw-materials/..%2Fusers made the page call another endpoint with the
viewer's token. Ids are now encodeURIComponent-ed; a regression test pins
"../users" to its own path segment.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Pages for raw material lots (beeswax, propolis): list, create in grams
with optional apiary, detail with stock card (harvested / used in
production / withdrawn / remaining) and "used in" batches.
- Production batch form takes the raw material used (lot + grams) and the
batch page shows what it was made from - traceability both ways.
- Sidebar entry "Rohstoffe".
- Removes backend files that were committed into ApiCore-Frontend/src by
mistake in e52a605 (copies of ApiCore-Backend/src/raw-materials).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Security review findings on the raw material commit:
- Creating a production batch with inputs books raw material off other
lots; production-batch:create alone no longer suffices, the user also
needs raw-material:withdraw (403 otherwise).
- Correcting the quantity of a raw lot or production batch below what was
already consumed, sold or withdrawn is refused (row lock + check in the
same transaction) instead of silently driving the remaining stock
negative; shortfalls go through an inventory correction.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- RawMaterialLot (beeswax, propolis) in grams with origin (apiary,
harvest); stock in the same movement ledger: withdrawals, inventory
counts, and the new movement type CONSUMPTION.
- Production batches take inputs (raw lot + grams); batch and consumption
are one transaction, never more than a lot has left. Deleting a
mistaken batch gives the material back as its own movement.
- Traceability both ways: batch -> inputs, raw lot -> usedIn.
- Movement belongs to exactly one of bottling / production / raw lot
(CHECK); raw material changes do not trigger the shop webhook.
- Permissions raw-material:*, granted to roles that maintain production
batches.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- New article page /products/[id]: remaining stock summed over all
production batches (or bottlings for honey), each batch linked to its
own page for withdrawals and counts; "Bestand ansehen" in the list.
- Production batches can be corrected (lot number, date, quantity,
best-before date, note) via PATCH; the article stays fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Bottling detail shows Abgefüllt / Webshop verkauft / Entnommen / Rest
(negative rest flagged "Prüfen") and the full movement list; buttons
"Entnehmen" (reason, note) and "Inventur korrigieren" (counted
quantity, difference booked).
- Rest column in the bottling list.
- New pages for production batches (list, create, detail with the same
stock card), sidebar entry with its own permission.
- Products get "Art" (honey vs. manufactured goods) and the webshop
category; bottlings only offer honey articles.
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]>
One remaining quantity per batch, kept in ApiCore: movement ledger for
bottlings and new production batches, shop write-back of paid sales and
returns, withdrawals in ApiCore reflected in the shop via webhook plus a
10-minute reconcile.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- DetailHeader: "Abfüllung anlegen" as the main action, PDF export in
"Mehr", state as icon + word, retry and way back on load errors.
- Traceability says what it shows and offers a retry on errors instead
of "Lädt…" forever.
- Removing a container allocation asks first and says what changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Same DetailHeader as colonies/queens: an h1, a 44 px back link, state as
icon + word, the obvious next step as the one primary button (harvest:
"Schleuderung erfassen"); a failed load offers a retry and a way back
instead of a bare red line. Colonies of a harvest are tappable links;
changing equipment condition confirms with a toast.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Drafts were stored in localStorage by form only, so on a shared phone
the next person was offered the previous user's unfinished entry
(colony observations, treatment and supplier details). Drafts are now
keyed by the signed-in user id and every draft is removed on logout,
like the offline cache already was.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
New DetailHeader: heading, back link, status as icon + word, ONE primary
action and a "Mehr" menu for exports, QR and the rest.
- Colony: "Durchsicht erfassen" is the main action; sub-cards show a
load error with retry instead of "Noch keine …".
- Apiary: error states, weather with units.
- Queen: edit card in sections, evaluations as 1-5 tap scales.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The feeding statistics summed every amount regardless of its unit, so
5 l syrup and 2 kg fondant showed as 7. The API now also returns byUnit
(units normalised: "L" = "l"), the old fields stay for compatibility,
and the statistics page shows one list per unit.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Varroa, swarm control, harvest, extraction, bottling, task and queen
forms use the form kit: required fields marked, inline errors with focus
on the first problem, rare fields folded, drafts, save in thumb reach.
Single choices (varroa method, swarm mood/action, priority, marking
colour) are large tap buttons; colonies and harvests are picked as
tiles. Plain hints: per-day mite fall vs. threshold, Brutto/Netto,
water content, MHD and lot number, marking colour by year.
Fixed on the way: the queen form suggested the marking colour from the
first digit typed ("2" -> Gelb for 2026); it now waits for a full year.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Every query shows loading, an error with "Erneut versuchen", an empty
state with a next step, then data; statistics no longer hang on
"Lädt…" after an error, a failed search no longer reads as "no hits".
- Storage: one primary action, stock items as a clearly secondary
section, low stock as icon + word.
- Wintering: "Gassen" explained, outcome as icon + word, touch-size
edit buttons; map: tells how many apiaries have coordinates.
- Statistics: units and plain words instead of "Ø", a select instead
of crowded tabs on phones.
- QR code buttons are touch size.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The Delete key removed a label element for good; it now shows a toast
with "Rückgängig" that puts it back in place. Closing or reloading the
designer with unsaved changes asks first.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Webhooks: one card per connection, events by their German label
instead of keys like "product.stock_changed", state as icon + word,
a one-sentence explanation of what a webhook is for, retry on errors.
- Calendar sync: the group id field says where to find it; the
"not configured" note says who sets it up instead of listing
environment variable names.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Inspection, treatment, feeding, varroa and swarm-control edit dialogs use
the form kit (required markers, inline errors, 1-5 as buttons, yes/no
tiles, number keyboards, save button pinned at the dialog bottom).
Moving, splitting and merging colonies report success; merging - which
cannot be undone - asks first and says what happens.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Lists load through QueryState: a failed load says so and offers
"Erneut versuchen" instead of looking empty; empty lists link to the
create page; exports move into a "Mehr" menu so "Neu …" is the one
primary action.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Same structure as the inspection form: required fields marked, errors at
the field, numbered sections with rarely used fields folded, drafts kept
on the device, save button in thumb reach, plain hints for jargon
(Wirkstoff, Wartezeit, Code, SKU, units). The Bestandsbuch fields of a
treatment stay a visible section of their own. Apiaries can take the
coordinates from the phone's location. Offline queue and idempotency
are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Users: one card per person on every screen size, status and MFA as
icon plus word, server reasons (e.g. "more rights than you") shown.
- Audit log: names instead of user ids, area as a choice instead of a
free-text type, optional record id, filter reset, cards instead of a
5-column table, empty and error states with a next step.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Phones get a list of the days that have entries (44 px rows with type
word, icon and title) instead of a 7-column grid with 0.7 rem chips.
- Month grid on wider screens: days are buttons, the chosen day lists
all its entries below, so "+N weitere" is no longer a dead end.
- Today is marked with a filled circle and the word "Heute"; a "Heute"
button jumps back. Month navigation buttons are touch size.
- Load errors offer a retry; an empty month says so.
- Entries are grouped by local day instead of UTC, so late-evening
entries no longer land on the next day.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Tabs ordered from everyday (suggestions, recurring tasks, labels) to
technical (calendar sync, API keys, webhooks); the page opens on
"Vorschläge" instead of Webhooks, and says in one sentence what it is.
- Resetting a number counter, deactivating a label template and ending a
recurring task now ask first and say plainly what happens.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Inputs and selects are 44 px on phones (36 px from sm up); default and
small buttons get 40/36 px on phones.
- Dialogs scroll inside the viewport, so long edit forms keep their
save button reachable on a phone.
- Dialog zoom, sheet slide, sidebar icon scale and stat-bar growth only
move at the motion level "Voll" (motion-full), not at "Dezent".
- QueryState / ErrorState / LoadingState: loading, error with "Erneut
versuchen", empty, data - a failed load can no longer look like an
empty list. EmptyState takes an action for the obvious next step.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
AuDHD-oriented rework of the most-used form, plus shared building blocks
for the remaining forms:
- Field: label, "Pflicht"/"optional", hint and the error right at the
field instead of a vanishing toast.
- FormSection: numbered steps; optional parts start folded and say how
many fields they hold.
- RatingScale (1-5 as five 44 px buttons with both ends named),
ToggleChip (yes/no tiles with tick and colour), StickySubmit (save
stays in thumb reach on phones).
- useFormDraft + DraftBanner: an unfinished entry is kept on the device
and offered back explicitly ("Weitermachen" / "Verwerfen"), never
applied silently; drafts expire after 3 days.
Inspection form: 21 flat fields became 5 steps, only colony and date are
required, observations are single taps, a hint appears when neither queen
nor eggs were seen, server messages show inline.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Auth flow and guard order as built now, new architecture decisions
(user-management escalation guard, SSO identity binding, sealed backups),
a section on API security with the remaining known gaps, and the audit
table with commits, new env variables and steps after the update.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Every JWT carries a jti. The SSO completion token and the MFA challenge
(once a code was accepted) are recorded as used in ConsumedToken; a
replayed request or copied cookie cannot open a second session. Used
ids are cleaned up after they expire.
- The token-validity check (active user, active role, password change)
reads fresh on every request and refreshes the permission cache, so a
deactivation or role change made on another replica applies to the very
next request instead of up to 30 s later.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Public lot lookup: 30 requests/min per IP and only well-formed lot
numbers (1-64 chars) reach the database.
- SSO callback URL is built from OIDC_REDIRECT_URI instead of the Host
header; SSO cookies are cleared with the attributes they were set with.
- Logout is rate-limited like the other auth endpoints.
- Middleware errors with a 4xx status (body too large, malformed JSON)
answer with that status instead of a 500; error log lines carry the
path without query string.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
search:read alone returned apiary names, colony and queen codes, task
titles and batch numbers to roles without the matching read permission.
Each kind of result is now searched only when apiaries:read,
colonies:read, queens:read, tasks:read or honey-batches:read is held.
The search term is validated (string, at most 100 characters).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- New passwords (create user, admin reset, own change) need at least 12
characters (ASVS 2.1.1) and may not exceed 72 bytes, which bcrypt would
silently cut off.
- They are checked against Have I Been Pwned with k-anonymity (only the
first 5 SHA-1 characters leave the server, padded responses); an
unreachable service is logged and does not block. PASSWORD_BREACH_CHECK
=false disables it for offline deployments.
- Login accepts existing passwords as before, capped at 256 characters to
bound anonymous bcrypt work.
- Password fields in the UI say the rule up front.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Typed-in values (colony codes, batch numbers, honey types) starting with
= + - @ TAB or CR were written as-is and ran as formulas (HYPERLINK, DDE)
when the export was opened in Excel or LibreOffice. Such text cells now
get a leading apostrophe; numbers keep their sign. Carriage returns are
quoted as well.
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]>
- ThrottlerGuard runs first, so requests with wrong tokens or API keys
count against the rate limit before any database lookup.
- TRUST_PROXY (hop count or Express list) replaces the hard-coded 1.
- Every API answer except the public lot pages is Cache-Control: no-store
without ETag; helmet sends default-src 'none' / frame-ancestors 'none'
(Swagger keeps the page policy when enabled) and same-site CORP.
- Request log lines carry the path only - no query strings (the SSO
callback's code and state no longer reach the logs).
- In production the server refuses to start without an https
FRONTEND_ORIGIN or with COOKIE_SECURE=false.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Rotation claims the old token with a conditional update; of two
concurrent refreshes only one gets a successor, so a stolen token cannot
fork into two live sessions.
- A token rotated out less than 10 s ago (parallel tab) is refused without
revoking every session; later reuse still revokes them all.
- Sessions end at most SESSION_MAX_AGE (default 7 days) after login: the
login time is inherited by every rotated token and caps its expiry.
- The refresh cookie is cleared with the same attributes it was set with.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Per-account lockout: five failed attempts (password, MFA code or a
step-up confirmation) lock the account for 15 minutes, on top of the
per-IP throttle - guesses spread over many addresses no longer help.
- A TOTP code is accepted once: the time step is stored and that step or
any earlier one is refused (also between two concurrent requests).
- Login runs bcrypt on every path and answers unknown email, wrong
password, deactivated and locked accounts with the same message; failed
logins for unknown emails no longer write attacker-chosen text into the
audit log.
- Turning MFA off needs password and a current code; turning it on needs
the password. Both end all other sessions.
- New audit action ACCOUNT_LOCKED.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Deliveries connect through a lookup hook that checks the address
actually dialled, so a host that resolves to a private address after
registration (DNS rebinding) or a tampered row cannot reach internal
services. Redirects are still not followed.
- More blocked ranges (CGNAT/100.64/10 incl. cloud metadata, multicast,
reserved, benchmarking, NAT64, 6to4, site-local, IPv4-mapped IPv6);
only ports 80/443, no credentials in the URL, https in production.
- 5 s timeout, response body discarded; dispatch no longer waits for
receivers, so a hanging endpoint cannot stall the user's request.
- Signature covers "timestamp.deliveryId.body" with X-ApiCore-Timestamp
and X-ApiCore-Delivery headers, so receivers can reject replays.
- Signing secrets are stored AES-256-GCM-encrypted; older plain rows still
work.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
backup:configure could point automatic backups at any writable directory
(e.g. a statically served one) and set a cron of every second; an invalid
expression deleted the running job before failing, silently ending
backups.
- A path entered in the UI is resolved under BACKUP_ROOT (default
./backups) and refused if it leaves it; env values stay unrestricted.
- The schedule must be a five-field cron that runs at most hourly.
- reload() validates before replacing the running job.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Backups carried every password hash, encrypted MFA secret and webhook
secret in plain JSON, and an import upserted arbitrary rows - including
users - so anyone holding backup:import, or tricking an admin into
importing a file, could overwrite the admin account with a known password.
- Export (download and scheduled) is AES-256-GCM over the gzipped bundle,
key from BACKUP_ENCRYPTION_KEY or derived from JWT_ACCESS_SECRET via
HKDF; header is authenticated, decompression is capped. Scheduled files
are written 0600 in a 0700 directory; run-now returns only the file name.
- Import requires the current password (and TOTP when MFA is on).
- Only a sealed file restores users, roles, grants, webhooks and audit
entries; an old unsealed file restores business data only and reports
what it skipped.
- Audit entries are append-only on import.
- Imported documents: entityType checked against the enum, MIME type
allowlisted and matched against the file signature, path kept under the
storage root (no more directory traversal via entityType).
- Permission cache is cleared after an import.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
SSO picked the local account by the email/preferred_username claim, which
Entra does not verify. Anyone able to present a matching email (another
tenant with a multi-tenant issuer, or an editable attribute) could sign in
as that user without a password.
- The first SSO login links the account to "<iss>|<oid>" (new unique
column User.ssoSubject); afterwards only that identity signs in, and a
matching email from another identity is refused.
- Multi-tenant issuers (common/organizations/consumers) disable SSO.
- Emails are compared case-insensitively and stored lower-case, so
[email protected] and [email protected] can no longer be two accounts; the migration
lower-cases existing emails where that does not collide.
- An admin changing a user's email clears the link.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A role with users:update could reset an administrator's password or MFA,
change the admin's email to one it controls, or give a colluding account
the ADMIN role (with users:assign-role) - and so become administrator.
Every management action (update, deactivate, activate, reset password,
reset MFA) and every role assignment now requires the target role's grants
to be a subset of the caller's own, like role editing already did. The
target role is read from its grants, so a deactivated admin still counts
as an admin. Changing another user's email revokes their sessions.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
All JWTs (access, MFA setup, MFA challenge, SSO completion) were signed
with the same secret and the access-token strategy checked only signature
and expiry. The MFA challenge token returned after the password step
therefore worked as a full access token: a password alone was enough to
use the API, skipping the second factor.
Every token now carries a pinned issuer, algorithm (HS256) and a
per-purpose audience; the strategy accepts only the access audience and
rejects any payload carrying a helper-token purpose. Existing access
tokens (no audience) stop working; clients renew them via the refresh
cookie.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- WinterCheck per colony, winter and season (autumn/spring), every field
editable; loss causes limited to the COLOSS monitoring categories.
- Summary per winter and apiary: loss rate lost/wintered, marked
provisional while colonies are not yet checked in spring - an unchecked
colony never counts as survived.
- A loss marks the colony deceased only on explicit request, in the same
transaction and audited.
- Permissions winter-checks:read/create/update granted by migration to
beekeeper and staff roles; winter checks included in backups.
- Page "Überwinterung": next action first, one button per row until the
colony is wintered, German number format.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Treatment records supplier, purchase proof and prescribing vet, and
keeps the apiary the colony stood at on the treatment day.
- Holder name and address in site settings (never in public output;
regression test in public.service.spec).
- treatment-log.pdf: one sheet per apiary, identical treatments grouped,
missing mandatory fields printed as FEHLT, uncertain locations marked.
- Shared PDF table helpers extracted from the record card.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
New report colony-record-card (button "Stockkarte (PDF)" on the colony
page): master data, current queen and queen history, every inspection as
a table row (strength, eggs, open/capped brood, queen seen, queen cells,
gentleness, swarm drive, feed, measures, notes), treatments with batch
number and withdrawal period, feedings, varroa counts and harvests.
Landscape A4, table header repeated on each page. Whole history by
default, one calendar year with ?year=.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Two new rules, "Kontrolle nach Behandlung" and "Kontrolle nach
Fütterung", create a check task when a treatment or feeding is recorded.
The due date counts from the measure itself (end of treatment, else its
date; feeding date) plus the configured number of days, and the task
names the product and date. At most one open check per colony and rule.
Both rules ship switched off and without an interval: the right timing
depends on the product (package leaflet) and season, so the beekeeper
sets it. Every task-creating rule can now carry a "due after N days"
setting in the automation page.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Label sheets printed on plain A4 are cut with a lever paper cutter. Each
template can now print small hairline cut marks in the sheet margin: one
at both ends of every cut line (left/right edge of each column, top/bottom
edge of each row), 1 mm away from the label and never closer than 2 mm to
the paper edge, so they print on normal printers and never touch a label.
Labels without a gap share one mark. Marks are drawn on every sheet for
the full grid, since the cutter cuts across the whole sheet.
On by default (new column cropMarks, default true); switch it off in the
template designer for pre-die-cut label sheets.
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]>
GET /api/v1/shop-sync returns exactly the data ApiCore releases to the
shop: harvest facts, article master data and bottlings with their
harvest ids. Every query uses an explicit select, so notes, colonies,
treatments and fields added to the models later never leave ApiCore
unless deliberately added to the allowlist.
- New permission shop-sync:read (granted to ADMIN automatically, to no
other role): the shop's service account needs nothing else, in
particular no access to harvest or extraction records with notes.
- contractVersion 1; only breaking changes bump it.
- Always the full set: small volume, and a full comparison heals missed
runs by itself.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Start page (AuDHD-oriented, per the agreed "Heute zuerst" layout):
- One highlighted next step: started work first (easy re-entry after an
interruption), then overdue, due today, then urgent undated tasks.
Primary action "Erledigt", secondary "Beginnen", link to the related
colony/apiary/queen.
- "Außerdem heute": at most five further tasks with a large honey-cell
checkbox; undated automatic suggestions are included so a queenless
colony does not sit unseen in the full list.
- Completing is the one authored motion moment: the cell fills, the tick
draws, the row folds away and the next step arrives with a short focus
pull. Every completion can be undone from the toast (10 s), restoring
the previous status.
- Status is shown as icon plus word (never colour alone); tasks created by
automation carry a "Vorschlag" badge, recurring and inspection tasks
their own - a suggestion never looks like work already done.
- Friendly empty, loading and error states; key figures moved into a
compact overview below; overdue tasks are no longer listed twice.
Motion: per-device setting Voll / Dezent / Aus in the profile (default
Voll); the OS reduce-motion setting always lowers Voll to Dezent.
New 44 px "touch" button size for gloves at the apiary; themed text
selection and caret.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
When the server saved an inspection, harvest, treatment, feeding, varroa
measurement or swarm check but the answer was lost (dead zone at the
apiary), the form queued the entry and the sync later created it a second
time - including a second automation task. Each opened form now creates
one Idempotency-Key, sends it with the request and stores it with the
queued entry, so the retry returns the first record. Entries queued before
this change still sync without a key.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Data-integrity fixes found in a systematic review of ApiCore:
- Bottling booked the honey batch amount from a value read outside the
transaction and wrote an absolute result back, so two concurrent
bottlings (double tap, two devices) could both pass the check and one
deduction was lost. Bookings now use a conditional UPDATE inside the
transaction; corrections and deletions give amounts back atomically.
- Outgoing stock movements could drive stock below zero; the removal is
now a conditional decrement that refuses when stock is insufficient.
- Weak, conspicuous and queenless colonies were treated as "not active"
and could not be moved, split or merged - uniting a queenless colony
is the textbook merge. Only terminal statuses lock a colony now; merging
a colony into itself and dissolving twice are rejected.
- Moving a queen to another colony failed on the unique currentQueenId
because the old colony kept pointing at her. The old colony now releases
her (and becomes QUEENLESS); a queenless colony receiving a queen becomes
ACTIVE; dissolved colonies cannot receive a queen.
- Completing a task twice moved completedAt and re-sent the webhook;
reopening a task kept completedAt. Recurring templates created tasks
without a due date and caught up missed weeks one stale task at a time.
- Automation rules created a new task and warning on every trigger (every
stock removal below minimum). Tasks now record their source trigger and
at most one open task per rule and object exists. The varroa threshold
(mites per day) is only compared with natural-mite-fall rates, not with
sample-based counts in another unit.
- New opt-in Idempotency-Key support for record-creating endpoints, so
the offline queue can retry after a lost response without duplicates.
- Permission catalog sync crashed replicas booting concurrently on a
fresh database (unique violation); it now uses createMany/skipDuplicates.
- Server-side validation for inspection rating scales (1-5), unique
colony/harvest lists and net weight <= gross weight.
- Stale e2e expectations updated (permission count, trigger list, real PNG
fixture for the file-signature check).
Migration is additive only (nullable Task column, new table).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The backend accepted every field of these records already; the forms simply
did not offer them, so from the app they were still frozen.
- Inventar: only the condition could be changed. A mistyped inventory number,
a wrong purchase or maintenance date, a renamed item - all stuck for the life
of the record. New edit dialog covering every field; the inline condition
select stays as a shortcut.
- Produkt: the status was shown as a badge but could not be set anywhere, so a
product could never be discontinued or brought back.
- Aufgabe: the status could only ever move to DONE through the "Erledigt"
button. IN_PROGRESS and CANCELLED were unreachable, and an accidentally
completed task could not be reopened.
- Aufgabenvorlage: priority, assignee and the active flag were missing.
Deactivating was one-way, since only a deactivate endpoint was wired up -
the active checkbox now reactivates through PATCH.
- Königinnen-Bewertung: notes and the evaluated colony were missing.
Verified by extracting the field set each form submits and comparing it with
the matching update DTO: every form now covers its DTO. The one gap left is
re-linking a document to a different record, which still has no UI.
Co-Authored-By: Claude Opus 5 <[email protected]>
renderLabelsPdf resolved the jarSize placeholder to "500 ml". Everywhere else
in the app jarSize is a fill weight in grams - the bottling forms label it
"Glasgröße (g)", the batch list and the public Herkunft page print "g", and
BottlingService divides by 1000 to get kilograms. Only the printed label said
millilitres, so every label produced so far declares the wrong net quantity for
a food sold by mass.
The placeholder mapping moved into an exported buildPlaceholderValues() so the
wording can be asserted directly. It could not be before: the render output is
a PDF buffer whose text a test cannot read, which is how the wrong unit shipped
unnoticed. Three tests now cover it, including one that rejects any volume unit
outright.
Labels already printed with "500 ml" have to be reprinted - the code change
does not correct paper.
Co-Authored-By: Claude Opus 5 <[email protected]>
test/jest-e2e.json listed the ESM-only packages ts-jest must still transform,
but @nestjs/schedule (12.0.1, "type": "module") was not among them. Since
AppModule imports ScheduleModule for the backup scheduler, every e2e spec died
on "SyntaxError: Unexpected token 'export'" while loading AppModule - the whole
suite, not one test.
That silently disabled test/authorization-coverage.e2e-spec.ts, the check that
asserts every non-public route carries @RequirePermission or
@AnyAuthenticatedUser. It is exactly the check that would have caught the
displaced decorators fixed in the previous commit, and it had been dead since
the scheduler shipped.
With @nestjs/schedule and its cron dependencies added to the allowlist, the
authorization-coverage spec compiles and passes, confirming no route is left
undecorated. The remaining 36 e2e specs still need a Postgres instance on the
port .env.test points at; they fail on connection, not on code.
Co-Authored-By: Claude Opus 5 <[email protected]>