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]>
Inserting the new PATCH handlers directly above an existing route put them
between that route's @RequirePermission decorator and its method, so the
decorator bound to the new handler instead:
- DELETE /documents/:id lost documents:delete
- POST /honey-batches/:id/measurements lost honey-batches:measure
- POST /queens/:id/evaluations lost queens:evaluate
and each new PATCH ended up carrying two @RequirePermission decorators, of
which only one takes effect.
This did not open anything up. PermissionsGuard fails closed by decorator
absence: a non-public route with neither @RequirePermission nor
@AnyAuthenticatedUser is denied outright, so the three routes would have
returned 403 for every caller including an administrator. The effect was a
broken endpoint, not a bypass.
Each decorator is back on the route it belongs to, and the duplicates are
gone. test/authorization-coverage.e2e-spec.ts asserts exactly this contract
across every route and is the check that should run before a route change
ships.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Both could only be created and deleted. For a webhook that is actively harmful:
recreating it issues a new signing secret, so the receiver that verifies the
X-ApiCore-Signature header against the old one starts rejecting every delivery
without anyone changing it on purpose.
Backend:
- New PATCH /webhooks/:id behind a new webhooks:update permission, covering the
target URL, the event selection and the active flag - the last of which had
no way to be set after creation at all, so a misbehaving receiver could only
be deleted. The signing secret is deliberately left alone and stays out of
the audit log.
- A changed webhook URL runs through the same assertPublicWebhookUrl SSRF check
as a new one; without that the edit would have been a way around the guard
create() applies. Covered by a test.
- New PATCH /documents/:id behind a new documents:update permission, for
re-linking a document filed against the wrong record and correcting its
display name. Re-linking validates the new target with the existing
assertEntityExists; the stored file is untouched.
Frontend:
- New webhook edit dialog. The event labels and the toggle helper moved out of
the create dialog into lib/webhooks/labels.ts so both use one copy.
- Documents can be renamed from the attachment card.
Re-linking a document to a different record has no UI yet: it needs a two-step
picker (colony, then inspection) that is not worth building unprompted.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Both records could be created but never changed. A water-content reading
decides whether a batch may be sold as honey at all, and queen ratings drive
breeding decisions - a slipped digit in either is exactly the kind of wrong
value that must not be left standing.
- New PATCH /honey-batches/measurements/:measurementId behind
honey-batches:measure, and PATCH /queens/evaluations/:evaluationId behind
queens:evaluate. Both are addressed by the child record's own id, so the
routes take two path segments and do not collide with PATCH /:id.
- Both log before and after data, like every other correction.
- The measurement list on the honey batch page and the rating list on the queen
page gained an edit dialog; the rating dialog reuses the existing criteria
list rather than restating it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Four fields were still frozen because changing them would have bypassed the
record that makes the change traceable, and a user's login data could not be
corrected at all. Locking a field is the wrong trade: a wrong value stays in
the system forever. The fix is to let the edit write the same history entry the
dedicated endpoint writes.
- A colony's apiary is editable through PATCH and writes the same
ColonyApiaryHistory entry POST /colonies/:id/move writes, so the location
history never skips a stand the colony was recorded on.
- A honey batch's storage container is editable through PATCH and writes the
same HoneyBatchRelocation entry POST /honey-batches/:id/relocate writes.
- A stock item's quantity is editable and books the difference as an ADJUSTMENT
movement, so the stored level stays the sum of its movements.
- A role's key is editable (system roles were already rejected outright by
RolesService.update, which keeps the seed's ADMIN lookup intact).
- A user's e-mail - the login identity - is editable. A typo there previously
locked the account out for good.
- New POST /users/:id/reset-password lets an administrator set someone else's
password: there is no self-service reset flow, so a forgotten password was
the one thing no edit could repair. It revokes every refresh token and clears
the permission cache exactly as a self-service change does, refuses to act on
your own account (that goes through /profile/password, which proves ownership
by asking for the current one), and never writes the password to the log.
Frontend:
- Users had no edit UI at all, although PATCH /users/:id existed. New
EditUserDialog covers name, e-mail, role and an optional new password.
- Colony, honey batch and stock item forms gained the newly editable fields,
each with a note saying the correction is recorded as a move, a relocation
or a stock adjustment.
- The role dialog gained the key field.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
An inspection, treatment, feeding, varroa measurement or swarm control check
filed against the wrong colony could not be repaired: the colony was frozen at
creation time, and with no delete endpoint either, the only remedy was leaving
a wrong record in the colony's history. The same held for a harvest booked
against the wrong apiary or with a colony forgotten, for an extraction linked
to the wrong harvests, and for a honey batch whose number or source extraction
was mistyped.
Backend:
- The five colony-scoped update DTOs now cover the full create DTO, colonyId
included.
- Harvest update accepts apiaryId and colonyIds, extraction update accepts
harvestIds. Both validate the ids first and replace the whole join-row set
inside the existing transaction; the join rows carry no data of their own,
so replacing beats diffing.
- Honey batch update accepts batchNumber, extractionId and status. Status was
previously only ever set to BOTTLED by the bottling module, so SOLD and
CONSUMED were unreachable through the API.
- Colony and storage container codes, and a task's relatedType/relatedId, are
editable.
- apiaryId on a colony and storageContainerId on a honey batch stay out of
PATCH on purpose: both have dedicated endpoints that also write the history
entry (ColonyApiaryHistory, HoneyBatchRelocation) the traceability chain
depends on.
- AllExceptionsFilter now also translates Prisma P2003 to a 400 and P2025 to a
404. Services write straight against the foreign keys with no application
level existence check, so before this a stale or mistyped relation id came
back as a bare 500 and filled the log with fake server errors.
Frontend:
- New shared fields: ColonySelectField (used by all five colony-scoped edit
dialogs), HarvestSelectField (both extraction dialogs) and TaskRelatedField.
- Re-filing a record invalidates the lists of both the colony it left and the
one it joined, otherwise one of the two stays stale.
- The harvest dialog gained an apiary select and a colony checklist that keeps
already-linked colonies visible even when they moved or were dissolved; the
honey batch dialog gained batch number, source extraction and status.
- Extraction and harvest forms refuse to submit an empty relation set instead
of letting the backend reject it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A bottling batch could only be corrected in four of its seven fields after
creation: honey batch, jar size and jar count were frozen at creation time,
an assigned product could not be cleared again, and there was no way to
remove a record entered by mistake at all. A mistyped jar count is a
data-entry error, not a fact of production, so it has to stay correctable.
Backend:
- UpdateBottlingBatchDto now covers every field. `productId: null`
explicitly clears the product assignment; omitting it leaves it alone.
- BottlingService.update rebalances the affected honey batch amounts:
the previously bottled amount is given back before the corrected amount
is taken again, so a correction never double-charges the source batch.
Moving a batch to a different honey batch credits the old one and debits
the new one in the same transaction.
- New DELETE /bottling-batches/:id gives the bottled amount back to the
honey batch and keeps the full record in the audit log (action DELETE,
beforeData). The honey batch status is deliberately left untouched - the
status it held before being bottled is not recoverable here.
- New permission bottling:delete, checked server-side. The catalog sync
grants it to system roles on the next boot.
Frontend:
- The edit dialog exposes all fields, offers "Kein Produkt" to clear the
assignment, keeps a discontinued product selectable so opening the dialog
cannot silently drop it, and shows backend errors inline.
- The form is mounted only while the dialog is open, so a cancelled edit
never leaks its values into the next one.
- New delete button behind a confirmation that names the amount credited
back and warns that the public origin page for the lot number stops
resolving.
- Edit and delete are additionally hidden without the matching permission.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
No code anywhere caught Prisma's P2002 (unique constraint violation).
Colony/apiary/queen codes, product SKUs, batch numbers, and equipment
inventory numbers are all @unique DB columns with no application-level
duplicate check ahead of the insert, relying on the constraint itself
- so a beekeeper reusing a code by mistake got a bare 500 "Internal
server error" with zero indication what went wrong, and the incident
polluted server logs as a fake error rather than a routine 4xx.
AllExceptionsFilter now recognizes P2002, returns 409 with a specific
German message naming the conflicting field when it's a single known
column (falling back to a generic duplicate message otherwise), and
logs it at warn level like any other client-side rejection.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Four more mutations had no error feedback: equipment condition change,
task completion, and notification mark-as-read all failed silently.
More importantly, the offline sync badge computed a result.failed
count from syncPendingInspections() but never displayed it - an entry
rejected by the server (e.g. a validation rule) would sit in the
offline queue indefinitely with zero indication to the beekeeper why
it never syncs. Both the failed-count and outright sync-mutation
rejection now surface a toast.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Backend business-rule guards (inactive apiary blocking colony
create/move, active colonies blocking apiary deactivation, negative
quantity on IN/OUT stock movements, unavailable queen blocking colony
assignment, inactive colony blocking split/merge/move) threw English,
UUID-laden exception text that the frontend either discarded in favor
of a generic message or - in colony-actions.tsx's move/split/merge/
assign-queen dialogs - didn't handle at all, so the guard fired with
zero visible feedback. Translated the messages to plain German and
wired the affected mutations to display them, so a beekeeper sees the
actual reason a save was rejected instead of silence or "failed".
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Two unguarded async calls silently swallowed errors: document download
used a bare "void downloadDocument(...)" with no catch, so a failed
download (network error, expired session) did nothing visible; the
webhook secret copy button had no try/catch around
navigator.clipboard.writeText, which can throw on denied permission or
an insecure context. Both now show a toast on failure.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Export mutations (annual report PDFs, colony/honey-batch CSV exports)
had no onError handler, so a failed download (network error, expired
session) silently did nothing with zero user feedback. Same silent-
failure class as the earlier onError sweep on entity-save mutations,
here applied to the 5 remaining export/download call sites.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Nearly every *-edit-dialog.tsx and *-edit-card.tsx in the app - the
save mutation for editing a colony, queen, treatment, inspection,
feeding, varroa measurement, swarm control check, harvest, extraction
(both variants), honey batch, bottling batch, task, stock item,
storage container, or product - had an onSuccess handler but no
onError. A failed save (a validation error, including several added
earlier this session, a permission error, a network hiccup) produced
no feedback at all: no toast, no inline message, the dialog just sat
there having silently done nothing. The user's most likely read on
that is "it saved," when it didn't.
Added the same onError: () => toast.error("<Entity> konnte nicht
gespeichert werden") pattern already used elsewhere in the app (e.g.
apiary deactivate/reactivate) to every one of these mutations. Also
covers assign-colony-dialog.tsx, which is the exact spot where the
queen-availability guard added earlier this session would otherwise
have failed with zero visible feedback.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The previous two commits added @Min(0)/@Max(100) validation and a
sign-inversion guard on the backend, but the number inputs themselves
had no min/max attributes - a user could still type a negative weight,
quantity, or an out-of-range percentage and only find out via a
generic "failed" toast after submitting, with no indication of why.
Added min={0} (and max={100} on water-content percentages) to every
input for a field the backend now bounds: harvest weights/super
count, honey-batch amount/water content, honey measurements, feeding
amount, extraction amount/water content, stock item quantity/min
threshold, and varroa mite count/duration/calculated value - across
both the create and edit form for each.
The stock-movement quantity input is the one exception requiring
care: min is only applied when type !== "ADJUSTMENT", matching the
backend guard, since a correction movement must still be allowed to
go negative. Added a short explanatory line under that field too.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
None of these numeric business fields had a lower bound, so a
negative honey weight, jar count, feeding amount, mite count, or
stock quantity was silently accepted - each would corrupt whatever
total or statistic it later feeds into. Added @Min(0):
- Harvest: honeySuperCount, grossWeight, netWeight
- HoneyBatch: amount (+ @Max(100) on waterContent, a percentage)
- HoneyMeasurement: waterContent (+ @Max(100))
- Feeding: amount
- Extraction: amount (+ @Max(100) on waterContent)
- StockItem: quantity, minThreshold
- VarroaMeasurement: durationMinutes, miteCount, calculatedValue
Left temperature fields alone (extraction, inspection) - genuinely
can be negative (winter inspections, outdoor storage) and no
confident domain bound exists to apply instead. Update DTOs for these
entities all extend PartialType(PickType(Create...Dto, [...])) from
@nestjs/swagger, which preserves validation decorators, so no
separate change was needed there.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
addMovement() negates quantity internally for OUT (delta = -quantity)
and applies it directly for IN. Nothing stopped a negative quantity
from being submitted, so e.g. { type: 'OUT', quantity: -5 } would
compute delta = -(-5) = +5 - silently ADDING stock while being
recorded in the audit trail as a removal. The inverse held for IN.
ADJUSTMENT is the one type that must stay allowed to be negative,
since that's the only way to correct a stock count downward, so this
is a service-level guard (BadRequestException) rather than a DTO
validator - class-validator's @ValidateIf skips every validator on a
property when its condition is false, including @IsNumber, so it
can't express "always a number, but only sometimes positive" cleanly;
verified this empirically before choosing the service-level approach.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
No error.tsx existed anywhere in the app - an uncaught render error on
any page (a beekeeper's most field-critical moment: mid-inspection,
no signal, nowhere to go) showed nothing recoverable, not even a
message, let alone a way back to a working page.
Two boundaries, matching Next.js App Router's error-boundary scoping:
- (app)/error.tsx covers every authenticated page. It only replaces
the crashed page's content - the sidebar/header/bottom nav stay
mounted and usable, so the user can navigate to a different page
without a full reload.
- app/error.tsx is the outer fallback (login, the public Herkunft
page, or a crash in (app)/layout.tsx itself, which only a strictly
higher boundary can catch). Its "Zum Dashboard" action is a plain
<a> reload rather than client-side navigation, since a crash this
high may have left client state corrupted.
Both are in German, matching the app's existing card/honeycomb visual
language, and explicitly reassure the user that already-saved data is
unaffected.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The colony annual PDF report's treatment section only ever printed
date/product/amount - activeIngredient, batchNumber, and
withdrawalPeriodDays (added earlier this session specifically for
Wartezeit compliance) existed on the Treatment record but never made
it into the one document a beekeeper would actually hand to a
Veterinäramt inspector or keep as their Bestandsbuch-style compliance
record. Silently missing the withdrawal period from the printed
record undermines the whole point of tracking it.
Also adds the first tests for annual-reports.ts (previously untested).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The service worker only ever cached same-origin static assets/pages -
every real data call (colonies, apiaries, tasks, honey batches, ...)
was excluded, both because it targets a different origin (API_URL
points at a separate backend service, per .env.example) and because
of an explicit /api path exclusion. In practice this meant a
beekeeper opening the app at an apiary with no signal saw nothing at
all: not even data fetched minutes earlier on the same device, despite
the offline write-queue built earlier assuming the app itself stays
usable while offline.
The backend can't be matched by origin the way static assets are (the
service worker has no static knowledge of the runtime-configured
API_URL), so it's matched by path instead - every backend call goes
through /api/v1/. /api/runtime-config is explicitly excluded and
always fetched live, since a stale cached value would point the app
at the wrong backend. Error responses (4xx/5xx) are now never cached
either, closing a latent bug that could otherwise serve a stale 401
back after a token refresh.
Since the cache now holds real business data, it's cleared on logout
so it can't leak into the next person's session on a shared device.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The colony-apiary and queen-availability validation just added
backend-side had no matching frontend guard, so a user could still
pick an inactive apiary or open the assign-colony dialog on an
unavailable queen and only find out it fails after submitting:
- Colony create/move apiary pickers now fetch only ACTIVE apiaries.
- The queen "Volk zuweisen" button is disabled once her status is
LOST/REPLACED/SOLD/DECEASED (QUEEN_UNAVAILABLE_STATUSES, mirroring
the backend's QUEEN_NO_LONGER_AVAILABLE_STATUSES), instead of only
failing after the form is filled out and submitted.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
assignColony() never checked the queen's own status, so a queen marked
LOST/DECEASED/REPLACED/SOLD - exactly the statuses the queen-loss fix
now treats as "not really available" and releases her from her old
colony for - could still be assigned to a brand new colony, which
makes no domain sense (you can't put a dead queen in a hive).
Rejects with ConflictException up front, before even looking up the
destination colony; correcting a mistaken status update first is the
intended fix path.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Colony.apiaryId was never checked against the target apiary at all -
create() would hit a raw Prisma FK-constraint error for a nonexistent
apiary instead of a clean 404, and both create() and move() would
silently succeed against a deactivated apiary, orphaning the colony
the same way (just from the opposite direction) as the apiary-side gap
fixed in the previous commit.
Adds a shared assertApiaryActive() check, following the same
findUnique-then-status pattern already used for cross-entity checks
elsewhere (e.g. QueensService.assertMotherQueenExists), used by both
create() and move().
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Colony.apiaryId is a plain FK, never checked against the apiary's own
status. Without a guard, deactivating an apiary silently orphaned any
colonies still living there: they kept showing as ACTIVE, but their
apiary had disappeared from every "active apiaries" list, dashboard
count, and colony-creation picker - a confusing half-state with no
warning at all.
ApiariesService.deactivate() now throws ConflictException when active
colonies remain, so the beekeeper has to move or dissolve them first -
matching the same assertActive-style guard pattern already used
elsewhere (colonies, e.g. move()/merge()/split()).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Colony.currentQueenId is unique across colonies. dissolve() and merge()
set status to DISSOLVED/MERGED but never cleared currentQueenId, so a
colony that still had a queen when it was dissolved or merged kept
permanently "owning" her - any later attempt to assign that queen to a
different colony (e.g. requeening another colony with her) would fail
with a unique-constraint conflict, since the dead colony's stale
reference was still occupying that slot.
Both operations now clear currentQueenId and close the open
QueenColonyHistory row for that queen, mirroring the bookkeeping
assignColony() and the queen-status-transition fix already do
elsewhere. The merge survivor keeps its own queen; a merged-away
colony's queen is released, not auto-transferred, since which queen
"wins" a merge is a beekeeper decision.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
A very common real-world event - a queen dying over winter, absconding,
being superseded, sold, or replaced - had no side effect on the colony
record at all. Colony.currentQueenId kept pointing at a queen who was
no longer really there, colony.status stayed whatever it was, and the
QUEENLESS dashboard alert could never fire from this path.
QueensService.update() now detects a transition into
LOST/DECEASED/REPLACED/SOLD and, inside the same transaction, clears
currentQueenId on whichever colony currently points at that queen,
flips its status to QUEENLESS (unless the colony is already in a
terminal lifecycle status like DISSOLVED, which is left alone), and
closes the open QueenColonyHistory row - mirroring the bookkeeping
assignColony() already does when a queen changes colonies.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
formatGeneratedNumber() passed the admin-supplied {SEQ:N} width straight
into String.padStart() with no upper bound. A careless or malicious
format value (e.g. {SEQ:999999999}) would try to allocate a
correspondingly huge string - on the backend that runs on every single
honey-batch/bottling creation once such a format is saved, and in the
newly added frontend preview it would run on every keystroke while
typing the format. Caps the effective width at 20 digits, far beyond
any realistic sequence number, in both the backend implementation and
its frontend port so they stay identical.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Admins configuring the honey-batch/bottling auto-numbering format
(WH-{YYYY}-{SEQ:4}) had no way to see what it would actually produce
before saving - a typo'd placeholder or unexpected format silently
breaks every batch number from then on. When the rule is active, the
format card now renders the next number it would generate, live as
the format is edited, using the same formatGeneratedNumber logic the
backend uses (ported to the frontend, no shared package between the
two apps) and the real next sequence value (sequenceValue + 1).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Audit requested by the user after finding queens couldn't set
hatchYear: many edit dialogs (and some create forms) were missing
fields the backend DTOs already accept, so that data could never be
entered or corrected anywhere in the UI. Fixes per entity:
- Colony: status (WEAK/QUEENLESS/CONSPICUOUS) was never settable
anywhere, so the COLONY_ATTENTION dashboard alert could never fire.
Added as a dropdown restricted to the non-terminal statuses;
DISSOLVED/SOLD/DECEASED/MERGED stay on their dedicated confirm
actions, which also set deactivatedAt.
- Apiary: description was missing from the edit form.
- Inspection: 10 fields (weather, temperature, broodAmount,
broodPattern, openBroodPresent, cappedBroodPresent,
queenCellsPresent, swarmMood, gentleness, combBehavior,
honeySuperStatus) existed in the schema/DTOs but nowhere in the UI -
added to both the create and edit forms.
- Varroa measurement: durationMinutes was on the create form but
missing from edit.
- Feeding: batchNumber missing from both create and edit.
- Treatment: startDate, endDate, activeIngredient, batchNumber
missing from both create and edit.
- Task: assignedUserId had no UI anywhere; added to create/edit,
degrading gracefully if the current user's role lacks users:read.
- Extraction (honey-batch traceability variant): amount was missing,
while the other edit dialog for the same entity already had it.
- Bottling: productId missing from edit.
- Harvest: grossWeight missing from edit; grossWeight and
honeySuperCount missing from create.
- Queen: breeder, matingType, matingStatus, inseminationData,
motherLine, fatherLine added to the edit form (hatchYear/
markingColor/markingNumber were already fixed in a prior commit).
Left alone deliberately: fields the backend DTO itself excludes via
PickType (e.g. HoneyBatch's jarSize/jarCount/totalAmount, Bottling's
quantities) since mutating those post-hoc would desync inventory
deduction, and Colony's terminal statuses/code which have dedicated
lifecycle actions.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
VarroaMeasurement.calculatedValue was already used as "Milben/Tag" by
the VARROA_THRESHOLD automation (thresholdMitesPerDay) and the annual
PDF report, but no client ever actually computed it - the frontend
form only collects raw miteCount and durationMinutes. That meant the
threshold check was comparing a raw, un-normalized mite count against
a per-day threshold: e.g. 12 mites counted over 5 days (2.4/day,
healthy) would incorrectly trigger the same alert as 12 mites counted
over 1 day (a real problem).
VarroaService now derives calculatedValue = miteCount / duration in
days for NATURAL_MITE_FALL measurements, on create and (only when no
value exists yet) on update, without ever overwriting an explicitly
provided or already-stored value. Other methods (Puderzucker,
Waschmethode, Drohnenbrutkontrolle) use a different, non-time-based
metric and are left for manual entry rather than guessing an
unverified formula.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The backend has always accepted hatchYear/markingColor/markingNumber
(Queen DTOs, Prisma schema), and the queens list even already renders
a "Schlupfjahr" field - but neither the create nor the edit form ever
exposed inputs for them, so no user could actually set hatchYear
through the UI. That silently broke the OLD_QUEEN dashboard alert
just added: it can only ever fire for colonies whose queen has a
hatchYear, and there was no way to record one.
Both forms now include Schlupfjahr, Zeichenfarbe and Zeichennummer.
Marking color is auto-suggested from the hatch year using the
internationally standardized queen marking color convention (last
digit of the year -> white/yellow/red/green/blue), editable like any
other suggestion.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Queen.hatchYear was already captured but never used anywhere - no
signal existed for "this colony's queen is overdue for evaluation or
replacement," despite that being one of the most common productivity
and swarming risk factors in beekeeping. Adds an OLD_QUEEN dashboard
alert alongside the existing overdue-task/swarm-check ones, flagging
any active colony whose current queen has completed 3+ seasons.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Treatments (Ameisensäure, Oxalsäure, etc.) can require a legally
mandated withdrawal period before honey from that colony may be sold.
Nothing in the app tracked this before, so a beekeeper had no
safeguard against harvesting into an active withdrawal window.
Treatments now optionally record withdrawalPeriodDays (from the
product's Packungsbeilage). The new harvest form fetches each
candidate colony's active withdrawal window and shows an inline
warning naming the product and the date it clears, computed as
max(endDate ?? date) + withdrawalPeriodDays across a colony's
treatments, restricted to windows still in the future.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Schwarmkontrolle already captures nextCheckDate per check, but it was
only ever visible by opening that one colony's tab. During swarm
season a missed follow-up check can mean losing half the colony, so
this now surfaces as a dashboard alert (OVERDUE_SWARM_CHECK) like
overdue tasks already do - only the most recent check per colony is
considered, so a completed follow-up clears the warning.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
iOS Safari ignores the web manifest for home-screen icons entirely and
needs its own apple-touch-icon link, which Next.js only emits when
app/apple-icon.tsx (or a literal apple-icon.png) exists. Without it,
iOS falls back to a screenshot of the open page instead of the brand
mark. Mirrors the existing public/icon.svg design via next/og.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
TaskTemplate had zero seed data - a fresh deployment showed a
completely empty recurring-task list with no hint of what to even put
there. Seeds 12 YEARLY templates, one per month, covering the standard
German-beekeeping annual cycle (Frühjahrsdurchsicht, Schwarmkontrolle,
Einfütterung, Varroa-Restentmilbung/Oxalsäure, Wintersitz-Kontrolle,
etc.) so a new beekeeper gets a ready-made starting point instead of a
blank page. Runs once (guarded by a taskTemplate.count() check, same
idempotency shape as the existing admin-user seed) and only ever adds
rows - admins can edit or deactivate any of them afterward.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Temperature and rain were already tracked, but wind matters just as
much for whether it's safe/sensible to open a hive - bees get more
defensive and frames are harder to handle in wind. Adds
wind_speed_10m_max to the Open-Meteo request and a derived
inspectionSuitable flag per day (>=15C, <=1mm rain, <=25km/h wind;
null when a value is missing rather than guessing). The apiary weather
card now shows a clear "good/not good for a Durchsicht today" banner
plus wind alongside each day's forecast.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Only Durchsicht had an offline fallback (queue to IndexedDB, sync on
reconnect); Behandlung, Fütterung, Varroamessung, Schwarmkontrolle and
Ernte just failed outright with no signal - exactly the situation a
beekeeper is in at the actual hive. Generalizes lib/offline/db.ts and
sync.ts to six queues (version-bumped IndexedDB schema, existing queued
inspections preserved), wires the same "server rejected it vs. no
network" fallback into all five remaining */new pages, and updates the
sync badge to count/drain all six.
Also: doc-hygiene pass (ARCHITECTURE.md/TASKS.md) to replace the stale
pg_dump/CronJob backup plan with the in-app BackupModule that actually
shipped earlier this session.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The dashed rectangle drawn around every label slot (a cut/crop line)
wasn't needed and is gone, along with the width/height params that
existed only to draw it. Dates on the label (bottling date, MHD) now
use { day: '2-digit', month: '2-digit' } so single-digit values print
with a leading zero (05.03.2026, not 5.3.2026) - the default 'numeric'
formatting was dropping it.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
A batch that doesn't fit in one container (e.g. 125 kg across 5 x 25 kg
buckets) had no way to be represented - HoneyBatch.storageContainerId
is a single nullable FK. Adds HoneyBatchContainerAllocation as an
additive join table (container + amount per batch), independent of the
existing single-container/relocate() flow which still covers the
simple one-container case unchanged.
- GET/PUT/DELETE /honey-batches/:id/containers[/:containerId], reusing
the existing honey-batches:relocate permission.
- Assigning a container is an upsert (@@unique on batch+container), and
the total assigned is validated against the batch's own amount when
known.
- New "Aufteilung auf mehrere Behälter" card on the honey batch detail
page, alongside (not replacing) the existing storage card.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The sidebar nav sent both entries straight to their "new" creation
form, with no list page to ever browse what had been created -
Schleuderung creation didn't even redirect to a detail page, just to
/colonies, so a saved extraction left no trace anywhere in the UI.
- New /harvests and /extractions list pages (mirrors the existing
products/apiaries list pattern).
- New /extractions/[id] detail page + edit dialog, matching the
harvests one that already existed but had nothing linking to it.
- Sidebar "Ernten"/"Schleuderungen" now point at the list pages instead
of the creation forms.
- Extraction creation now redirects to its detail page instead of
/colonies.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
One Entra enterprise application now covers both interactive login and
the calendar sync's client-credentials Graph access, instead of
requiring a second app registration. GraphClientService reads
OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET (same as SSO) and
derives the tenant ID from the issuer URL's path, rather than three
dedicated M365_CALENDAR_* env vars. The app registration just needs the
"Calendars.ReadWrite" Application permission added alongside its
existing delegated login scopes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds a one-way push (ApiCore -> M365) of Aufgaben/Durchsichten/
Behandlungen/Fütterungen/Ernten/Königinnen-Zuordnungen into the calendar
of a Microsoft 365 group, via the Graph API.
- CalendarSyncModule (app-only client-credentials Graph auth, separate
from the interactive SSO app registration; new env vars
M365_CALENDAR_TENANT_ID/_CLIENT_ID/_CLIENT_SECRET).
- Outbox + minute-interval cron worker (CalendarSyncOutboxEntry) instead
of a synchronous push, since Graph calls are too slow/flaky to block a
domain create request on; failed syncs retry up to 5 times.
- Per-source-type toggle plus a group-ID field, configurable in
Verwaltung > Automatisierung > Kalender-Sync (Verwaltung > Backup, an
env var still wins on those fields where one applies to related config).
- Hooked into Tasks/Inspections/Treatments/Feedings/Harvests/Queens at
their existing create (and Task update/complete) points; entries render
as all-day Graph events to avoid timezone ambiguity.
- Documents the design decision in ARCHITECTURE.md (Annahme 9), since
this is a net-new capability the lastenheft doesn't scope.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2 links center side-by-side, 3 sit in one row, and 4 form a centered
2x2 block - previously flex-wrap could leave an uneven number stranded
awkwardly on its own line.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- SiteSettings gains websiteUrl (e.g. online shop link) alongside the
existing social links, editable in Verwaltung > Automatisierung.
- /herkunft/[lot] now shows the apiary's Standort instead of its internal
Name (so multiple apiaries at one location read as one origin), adds
small icons per social link, and surfaces previously-fetched-but-unused
data (Honigsorte, Abfülldatum, stat cards) to make the page a richer
customer-facing info page.
- Backup schedule (path/cron/retention) is now also configurable via the
GUI (Verwaltung > Backup), stored in a new BackupSettings singleton;
an env var always wins over the GUI value per field, and the scheduler
reloads its cron job live when GUI settings change.
- New Automatisierung rules let an admin define a format (e.g.
"WH-{YYYY}-{SEQ:4}") that auto-generates HoneyBatch.batchNumber and
BottlingBatch.lotNumber on creation, with an atomic per-rule sequence
counter and a manual reset. Manual entry still works when the rule is
inactive.
Backup export/import now covers Role, User, RolePermission,
Notification, AuditLog and Document (with actual file content), not
just business data - matching "wirklich alle Daten". Role/User use
natural keys (role.key, resource+action) so a bundle restores cleanly
into a different database.
Adds BackupSchedulerService (@nestjs/schedule) for cron-based automatic
backups to a local path (BACKUP_STORAGE_PATH), with retention pruning
and a file-mtime guard against duplicate runs across replicas. New
GET /backup/schedule and POST /backup/schedule/run-now endpoints, plus
a status card and manual trigger in the frontend Backup settings page.
Menü unter "Verwaltung" wie gewünscht neu strukturiert:
- "Benutzer & Rollen" (/settings/users-roles): Benutzerverwaltung und
Rollen & Rechte als zwei Reiter auf einer Seite (vorher zwei
getrennte Menüpunkte).
- "Automatisierung" (/settings/automation): Webhooks, Wiederkehrende
Aufgaben, Etikettenvorlagen und Social Media Links als Reiter
gebündelt - dazu (eigene Ergänzung, da sonst zwei fast gleich
benannte Menüpunkte "Automatisierung"/"Automatisierungen"
nebeneinander gestanden hätten) auch die bestehenden
Automatisierungsregeln als fünfter Reiter "Regeln".
- "Backup" (/settings/backup): neuer voller Daten-Export/Import
(Commit zuvor) plus der bisherige CSV-Import, beides auf einer Seite.
Aktiver Reiter wird als ?tab=... in der URL gehalten statt nur im
Client-State, damit z.B. "Speichern" im Etiketten-Designer wieder auf
den richtigen Reiter zurückführt.
Technisch: bestehende Seiteninhalte in Reiter-Komponenten extrahiert
(PageHeader entfernt, Rest unverändert übernommen), Dialog-Unterkomponenten
in die neuen Ordner verschoben (git mv, Historie bleibt erhalten). Die
Etiketten-Editor-Routen (/settings/label-templates/new, /[id]) bleiben
unverändert bestehen, nur der Rücksprung nach dem Speichern zeigt jetzt
auf den neuen Reiter. settings/layout.tsx's Berechtigungsliste um
label-templates:read, site-settings:read, backup:export/import ergänzt -
schließt nebenbei eine bestehende Lücke (eine Rolle mit ausschließlich
diesen Rechten wurde vorher faelschlich von der ganzen
Verwaltungssektion ausgesperrt.
Neuer Reiter-Baustein src/components/tab-switcher.tsx (generisch, nach
Vorbild von ActiveHistoryTabs). sidebar.tsx's NavItem.permission
unterstützt jetzt auch ein Array (irgendeine der Berechtigungen reicht),
fuer die neuen gebündelten Menüpunkte.
Lokal verifiziert: typecheck/lint/test/build fehlerfrei, Build zeigt
alle neuen Routen korrekt, Live-Test im Browser (ohne Login) zeigt
sauberen Redirect zu /login ohne Konsolenfehler.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Neues Modul src/backup/ fuer Backups und Datenuebertragung zwischen
Deployments: GET /backup/export liefert ein JSON-Bundle mit allen
fachlichen Daten (Bienenstaende bis Etikettenvorlagen, ~30 Tabellen),
POST /backup/import spielt so ein Bundle wieder ein.
Bewusst NICHT enthalten: User/Rollen/Rechte (werden ueber die
Benutzerverwaltung gepflegt, Permission-Zeilen werden ohnehin bei
jedem Start aus dem Code neu synchronisiert), Audit-Log (unveraenderliche
Historie), Notifications (fluechtig), Dokumente (nur die DB-Zeile waere
gesichert, nicht die tatsaechliche Datei auf der Festplatte).
Import laeuft in einer Transaktion, Tabellen in Abhaengigkeitsreihenfolge
(z.B. Bienenstand vor Volk, Volk vor Durchsicht). Selbstreferenzen
(Koenigin -> Mutterkoenigin) werden in zwei Durchgaengen aufgeloest,
damit die Reihenfolge im Export keine Rolle spielt. userId-Felder, die
im Zielsystem nicht existieren (z.B. Uebertragung auf ein anderes
Deployment), werden auf den ausfuehrenden Admin umgebogen statt die
Fremdschluessel-Pruefung scheitern zu lassen. AutomationRule wird ueber
triggerType statt id abgeglichen, da diese Zeilen code-fest sind.
Neue Berechtigungen backup:export/backup:import. 8 neue Unit-Tests
fuer die Sonderfaelle (Selbstreferenz, userId-Remapping,
AutomationRule-Abgleich, Audit-Eintrag).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Im automatischen Modus (Spalten/Zeilen leer) wird jetzt zusaetzlich
berechnet, wie viele Etiketten pro Blatt passen, wenn man sie um 90
Grad dreht (Breite/Hoehe vertauscht). Passt die gedrehte Anordnung mehr
drauf, wird sie verwendet - die Etiketten selbst bleiben inhaltlich
unveraendert (nur auf dem Blatt gedreht), jedes einzelne Etikett bleibt
beim Abziehen normal lesbar.
Bei explizit gesetzten Spalten/Zeilen (z.B. fuer ein vorgedrucktes
Avery-Raster) greift die Drehung nicht - ein reales Blatt hat feste,
nicht drehbare Felder.
Rotation erfolgt via pdfkit-Rotation um die Mitte des Feldes: ein
Etikett, exakt 90 Grad um seinen eigenen Mittelpunkt gedreht, ergibt
immer eine vertauschte Bounding-Box an derselben Stelle - unabhaengig
von der Drehrichtungskonvention. 2 neue Tests belegen ueber die
Seitenzahl, dass die Drehung greift, wenn sie mehr Etiketten
unterbringt, und dass ein explizites Raster unangetastet bleibt.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Der voran gegangene Fix (proxy.ts nach src/ verschoben, damit Next.js
sie ueberhaupt erkennt) hat eine zweite, bislang tote Stelle live
geschaltet: eine Cookie-Pruefung auf "refreshToken", die zu /login
umleitet, wenn das Cookie fehlt - fuer /settings, /apiaries, /colonies,
/queens.
Dieses Cookie wird vom BACKEND gesetzt (eigene Domain,
apicorebackend.* statt apicore.*, Pfad /api/v1/auth) und ist dem
Browser zufolge nie Teil einer Anfrage an das Frontend - proxy.ts lief
auf der Frontend-Domain und konnte dieses Cookie strukturell nie sehen.
Solange die Datei am falschen Ort lag und nie ausgefuehrt wurde, blieb
das folgenlos; sobald sie lief, hat sie jeden Aufruf dieser Seiten
serverseitig zurueck zu /login geschickt, unabhaengig vom tatsaechlichen
Login-Status.
Die Cookie-Pruefung komplett entfernt statt repariert: es gibt keinen
Weg, ein cross-origin gesetztes Cookie hier sichtbar zu machen, ohne
seinen Schutz (Pfad-Scoping) aufzuweichen. Die echte Session lebt
ohnehin nur clientseitig (Access-Token im Speicher, Refresh-Cookie nur
fuer Backend-Aufrufe) - src/app/(app)/layout.tsx prueft das bereits
korrekt und leitet bei Bedarf weiter. proxy.ts setzt jetzt nur noch die
Security-Header.
Lokal verifiziert: /settings/users ohne jedes Cookie liefert jetzt 200
statt eines Redirects, CSP zeigt weiterhin den korrekten
Laufzeit-API_URL.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Root Cause, zweifach:
1. proxy.ts lag im Projekt-Root statt unter src/ (wo app/ tatsaechlich
liegt). Next.js hat die Datei dadurch nie erkannt - ueberprueft via
Build-Output ("Proxy (Middleware)" fehlte komplett) und via
routes-manifest.json. Das betraf nicht nur die neuen
Security-Header, sondern auch die schon laenger bestehende
Login-Redirect-Logik, die dadurch vermutlich nie serverseitig
gegriffen hat - vorher unbemerkt, weil Client-seitige Auth-Checks
das kaschiert haben.
2. Die CSP (mit dynamischem connect-src aus API_URL) lag in
next.config.ts's headers() - die wird beim Docker-Build EINMAL
aufgeloest und ins Standalone-Output eingefroren, nicht bei jedem
Container-Start neu gelesen, wie im Code-Kommentar faelschlich
behauptet. Jedes Deployment bekam dadurch permanent
"connect-src http://localhost:3001" (den Fallback, da API_URL beim
Build nicht gesetzt ist) - unabhaengig vom tatsaechlich im Pod
gesetzten API_URL. Jede Anfrage vom Frontend ans Backend (Login,
SSO-Status, Herkunftsseite) wurde dadurch von jedem Browser
CSP-blockiert.
Fix: CSP + Security-Header ziehen nach proxy.ts (jetzt korrekt unter
src/) um - Proxy laeuft mit Node.js-Runtime frisch bei jeder Anfrage,
liest process.env.API_URL also korrekt zur Laufzeit. next.config.ts
behaelt nur noch die statische sw.js-Cache-Control-Regel.
Lokal end-to-end verifiziert: Build zeigt jetzt "ƒ Proxy (Middleware)"
(vorher komplett abwesend), CSP-Header zeigt nach Server-Start einen
distinct getesteten API_URL-Wert korrekt an (nicht mehr den
Localhost-Fallback), und ein echter Login-Klick im Browser schickt die
Anfrage jetzt an die richtige, zur Laufzeit gesetzte Backend-Origin.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Beim Umstellen von PermissionsGuard auf Fail-Closed wurde der
@Public()-Check vergessen. JwtAuthGuard ueberspringt bei @Public()
zwar die Authentifizierung, aber PermissionsGuard laeuft als
eigenstaendiger, separat registrierter APP_GUARD trotzdem mit - und
verweigerte seitdem jede Route ohne @RequirePermission/
@AnyAuthenticatedUser, selbst wenn sie @Public() war. Betroffen: health
und health/ready (die gemeldeten 403), aber ebenso Login, SSO-Flow und
die oeffentliche Herkunftsseite.
PermissionsGuard prueft jetzt @Public() als erstes und laesst diese
Routen durch, genau wie JwtAuthGuard es bereits tut. Regressionstest
ergaenzt.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Abfüllungen hatten keine Übersichtsseite - die Navigation ging direkt
auf /bottling/new, sodass eine angelegte Abfüllung danach nirgends
mehr sichtbar war. Neue Seite /bottling listet alle Abfüllungen
(Archiv), Klick führt zur bestehenden Detail-/Bearbeiten-Seite.
Navigationslink zeigt jetzt auf die Liste statt direkt auf "Neu".
Neues Modul site-settings (Singleton-Tabelle SiteSettings) für
YouTube-/Instagram-/TikTok-Links, verwaltbar unter Verwaltung > Website
(neue Berechtigung site-settings:read/update). Die öffentliche
Herkunftsseite (/herkunft/[lot]) zeigt die konfigurierten Links jetzt
zusätzlich zur Chargen-Herkunft an - PublicService liefert sie über
das bereits oeffentliche /public/batches/:lotNumber mit aus, keine
zusaetzliche unauthentifizierte Route noetig.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
JWT_ACCESS_SECRET war zwar als Pflicht-Env-Var geprueft, aber ohne
Mindestlaenge - ein kurzer/erratbarer Wert haette nicht nur das
Faelschen von Access-/MFA-/SSO-Tokens fuer jeden Nutzer erlaubt,
sondern auch (ueber mfa-crypto.util.ts, das den AES-Schluessel fuer
gespeicherte TOTP-Secrets aus genau diesem Wert ableitet) das
Entschluesseln aller gespeicherten MFA-Secrets.
main.ts bricht den Start jetzt mit klarer Fehlermeldung ab, wenn
JWT_ACCESS_SECRET kuerzer als 32 Zeichen ist. .env.example und das
K8s-Secret-Template zeigen jetzt einen ausreichend langen Platzhalter
mit Erzeugungshinweis (openssl rand -hex 32).
Betrifft nur main.ts's bootstrap() - e2e-Tests bootstrappen ueber
test/utils/create-test-app.ts direkt via configureApp() und durchlaufen
diese Pruefung nicht, daher keine Testanpassung noetig.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Ein zweiter, unabhaengiger Audit-Durchlauf (frischer Blick, keine
Vorkenntnis meiner eigenen Fixes) bestaetigte alle bisherigen Fixes als
korrekt verdrahtet und fand 3 LOW-severity Robustheitsluecken - keine
davon war ausnutzbar, aber alle sind jetzt behoben:
- HarvestsService.create/ExtractionsService.create pruefen jetzt, dass
referenzierte colonyIds/harvestIds tatsaechlich existieren, bevor die
Transaktion laeuft - vorher fuehrte eine unbekannte Id zu einem
ungefangenen 500 mit rohem Prisma-Fehler statt einer sauberen 400.
- Etiketten-Bildelemente durchlaufen jetzt denselben Magic-Byte-Check
wie Avatar-/Dokument-Uploads, statt pdfkits Bildparser ungepruefte
Bytes zu uebergeben.
- Webhook-Secret-Redaktion im Audit-Log nutzt jetzt explizites delete
statt sich auf Prismas JSON-Serialisierung zu verlassen, die
undefined-Werte verwirft (fragil, falls sich die Serialisierung mal
aendert).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
RolesService.create/update uebernahm eine explizit uebergebene
permissionKeys-Liste ungeprueft. Ein Nutzer mit nur roles:update
konnte damit JEDER Rolle - auch der eigenen, ohne users:assign-role -
beliebige weitere Rechte (bis hin zu allen 92 Katalog-Eintraegen)
hinzufuegen, da das Bearbeiten der eigenen Rollen-Rechte nicht ueber
User.roleId laeuft und daher vom bereits gefixten
users:assign-role-Check nicht erfasst wurde.
Neue Regel (Standard-RBAC-Prinzip "keine Rechte ueber das eigene
Rechte-Set hinaus vergeben"): create/update pruefen jetzt, dass der
aufrufende Nutzer jedes zu vergebende Recht selbst bereits besitzt,
bevor die Rolle angelegt/geaendert wird.
roles.service.ts hatte bislang keine Unit-Tests; neue Spec-Datei deckt
den Fix sowie die uebrigen Methoden (create/update/deactivate/
reactivate) ab.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Eine explizit im Etiketten-Designer eingetragene Spalten-/Zeilenzahl
wurde beim PDF-Rendern ungeprueft uebernommen. Passt sie nicht auf ein
A4-Blatt (z.B. 90mm breite Etiketten x 4 Spalten bei 5mm Rand), ragen
die hinteren Spalten/Zeilen ueber den Blattrand hinaus - der Drucker
schneidet sie dann einfach ab, statt sie zu drucken.
- Backend (label-templates.service.ts): Spalten/Zeilen werden jetzt auf
das tatsaechlich auf A4 passende Maximum begrenzt, bevor gerendert
wird - betrifft auch bereits gespeicherte Vorlagen mit zu grossen
Werten.
- Frontend (label-designer.tsx): zeigt die maximal passende Spalten-/
Zeilenzahl direkt im Seiten-Layout-Panel an und warnt inline, wenn
der eingetragene Wert das Blatt sprengen wuerde.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
PermissionsGuard erlaubte bisher jede Route ohne @RequirePermission
automatisch fuer jeden eingeloggten Nutzer (fail-open) und verliess sich
allein auf test/authorization-coverage.e2e-spec.ts, um eine vergessene
Decorator-Angabe vor dem Deploy zu erwischen - der Guard selbst hatte
kein Default-Deny.
Neuer Decorator @AnyAuthenticatedUser() macht das bisher stillschweigend
erlaubte Verhalten explizit fuer die 12 Routen, die es tatsaechlich
brauchen (ProfileController komplett, NotificationsController komplett,
AuthController.me - alle bereits vorher auf der Allowlist des e2e-Tests).
PermissionsGuard verweigert jetzt jede Route, die weder
@RequirePermission noch @AnyAuthenticatedUser traegt - eine vergessene
Decorator-Angabe schlaegt jetzt zur Laufzeit fehl (403), nicht mehr nur
im Test. Der e2e-Test prueft jetzt denselben Decorator-Vertrag statt
einer separat gepflegten Allowlist, kann also nicht mehr vom
Guard-Verhalten abweichen.
Neue Unit-Tests fuer PermissionsGuard decken beide Codepfade und das
Fail-Closed-Verhalten ab.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Weitere vom Sicherheitsaudit gemeldete Luecken behoben, die bisher als
niedrige Prioritaet offen gelassen wurden:
- SSO-Login (Entra ID) prüft jetzt lokale MFA genauso wie der
Passwort-Login: ist mfaEnabled gesetzt, liefert /auth/sso/complete
denselben mfaRequired-Codepfad wie /auth/login, bevor Tokens
ausgestellt werden. Frontend (Login-Seite, AuthContext) zeigt in dem
Fall den bestehenden Code-Eingabe-Schritt statt die Anmeldung
stillschweigend abzuschliessen.
- Avatar- und Dokument-Uploads pruefen jetzt die tatsaechlichen
Datei-Bytes (Magic Numbers fuer PNG/JPEG/WEBP/PDF) statt nur dem
client-angegebenen MIME-Typ/data:-URL-Praefix zu vertrauen.
- Swagger-UI (/api/v1/docs) ist jetzt standardmaessig deaktiviert
(ENABLE_SWAGGER=true erforderlich), da sie zuvor das komplette
API-Schema ungeschuetzt oeffentlich preisgab.
npm-audit-Warnungen (deepmerge-ts/mysql2 unter prisma) bewusst nicht
gefixt: kein stabiles Prisma-Release loest sie bislang auf, nur eine
Release-Candidate-Hauptversion - das Risiko eines Downgrades/RC-Upgrades
der Datenbankschicht uebersteigt den Nutzen fuer eine Kette, die zur
Laufzeit ohnehin nicht erreichbar ist (nur Prisma-CLI, nur MySQL-Treiber,
dieses Projekt nutzt Postgres).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Ein Sicherheitsaudit deckte auf, dass jede Rolle mit users:update sich
selbst per PATCH /users/:id auf eine Admin-Rolle heraufstufen konnte,
da roleId ohne eigene Berechtigung und ohne Selbstbezug-Pruefung
akzeptiert wurde. Ausserdem lag ein fest im Code hinterlegtes
Standard-Admin-Passwort vor, das bei fehlender SEED_ADMIN_PASSWORD-Env
greift.
- Neue Berechtigung users:assign-role, getrennt von users:create/update,
fuer jede Rollenzuweisung erforderlich
- Selbstaenderung der eigenen Rolle serverseitig blockiert (auch fuer
Admins), ebenso Selbst-Reset der eigenen MFA ueber die Admin-Route
- Seed erzeugt bei fehlender Env ein zufaelliges statt eines fixen
Passworts
- Wiederverwendung eines bereits rotierten Refresh-Tokens (Diebstahl-
Indiz) widerruft nun alle Sessions des betroffenen Nutzers
- Rate-Limit auf MFA-Enrollment-Bestaetigung ergaenzt
Co-Authored-By: Claude Sonnet 5 <[email protected]>
apicore.encry.de lieferte bisher gar keine Security-Header aus (per
curl bestaetigt) - CSP, X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, HSTS fehlten komplett, waehrend das Backend das
laengst per helmet() macht. Ergaenzt fuer alle Routen. Bewusst ohne
CSP-Nonce (haette dynamic rendering fuer jede Seite erzwungen und
damit die bestehende statische Optimierung gekostet) - stattdessen
script-src/style-src 'unsafe-inline', aber default-src/connect-src/
img-src/object-src/frame-ancestors/form-action/base-uri strikt
eingeschraenkt. Lokal mit dem echten Production-Build (standalone
server + next.config.ts) verifiziert: Login-Seite rendert/hydratisiert
sauber, keine CSP-Verstoesse in der Konsole, Header per curl bestaetigt.
POST /profile/avatar schlug live mit net::ERR_FAILED fehl (Verbindung
abgebrochen, keine Antwort) - selbst bei einer winzigen Testdatei.
CORS-Preflight lief sauber durch, nur der eigentliche multipart-POST
nicht; passt zum bereits dokumentierten Cloudflare-Verhalten bei
multipart-Uploads (siehe .gitea/workflows/*-image.yml). Umgangen durch
Umstellung auf denselben JSON-Content-Type, der fuer alle anderen
Endpunkte nachweislich zuverlaessig funktioniert: Bild wird
clientseitig als data:-URL kodiert und im JSON-Body gesendet.
Globales JSON-Body-Limit auf 5mb angehoben (deckt 2MB-Bild plus
Base64-Overhead).
Eine KI-erzeugte JSON ohne Kenntnis des echten Schemas (z.B. eigene
Feldnamen wie xMm/yMm oder erfundene Elementtypen wie rectangle/svg)
scheiterte bisher mit "Datei enthält keine gültige Etikettenvorlage"
ohne Hinweis, was falsch ist. Jetzt wird pro Element benannt, was
fehlt oder welcher Typ nicht unterstützt wird.
- LabelTemplate bekommt columns/rows (optional, leer = automatisch
passend), plus konfigurierbare Raender/Abstaende - damit ein Etikett
exakt auf das vorgedruckte Raster eines physischen Etikettenbogens
(z.B. Avery) gelegt werden kann statt nur automatisch berechnet
- Startposition beim Drucken waehlbar, fuer bereits angebrochene Boegen
(erste Seite startet ab der gewaehlten Position, Folgeseiten wieder
von vorne)
- Vorlage als JSON exportieren/importieren, damit man sich ein Etikett
auch per KI erstellen und danach importieren kann
- Zoom-Regler fuer den Canvas (50-200%), da Etikettgroessen stark variieren
- X/Y-Positionsfelder pro Element fuer praezise Platzierung statt nur Drag
- Element duplizieren-Button
- Entf/Backspace loescht das ausgewaehlte Element
DropdownMenuLabel (rendert Menu.GroupLabel) stand direkt im
DropdownMenuContent statt in einem DropdownMenuGroup - Base UI wirft
dafür beim Öffnen einen ungefangenen Fehler, der den kompletten
React-Baum crasht. Das war die eigentliche Ursache des seit Sitzungsbeginn
gemeldeten "This page couldn't load" beim Klick auf das Profilbild
oben rechts, nicht der Service Worker (der war schon korrekt).
git add -u im letzten Commit hat neue (untracked) Dateien übersprungen:
die Migration für LabelTemplate.elements, die neuen Typ-/DTO-Dateien und
den kompletten Drag-and-Drop-Editor fehlten dadurch im Build.
labels.ts und die ColonyCondition-Migration waren untracked und fehlten
im letzten Commit, dadurch schlug der Docker-Build mit "Module not
found: @/lib/colonies/labels" fehl.
ts-node prisma/seed.ts lief bisher live im Container - der
TypeScript-Compiler dafuer braucht deutlich mehr Heap als das
512Mi-Pod-Limit, daher sporadische "JavaScript heap out of memory"-
Crashes beim Start (manchmal lief's, manchmal nicht, reine
GC-Zufaelligkeit an der Speichergrenze).
Fix: seed.ts wird jetzt wie main.ts beim Docker-Build vorkompiliert
(tsconfig.seed.json, rootDir bewusst auf prisma/ statt Projekt-Root
gesetzt, damit der relative Import '../generated/prisma' nach dem
Kompilieren weiterhin auf denselben Pfad zeigt). Laufzeit-CMD startet
nur noch plain `node dist/seed.js` - kein TypeScript-Compiler mehr im
Container. ts-node/typescript zurueck zu devDependencies (nur noch
fuer den Build-Schritt gebraucht, nicht mehr zur Laufzeit).
incremental in tsconfig.seed.json explizit deaktiviert - ein stale
tsconfig.seed.tsbuildinfo liess tsc beim zweiten Testlauf sonst
faelschlich "nichts zu tun" melden (derselbe Fallstrick wie bei
tsconfig.build.tsbuildinfo).
Verifiziert: tsc/eslint clean, 304/304 Tests, Build reproduzierbar
(zweimal hintereinander ohne Cache-Reset getestet), node dist/seed.js
lief erfolgreich gegen die echte lokale DB (erkannte den bestehenden
Admin korrekt und legte ihn nicht erneut an).
Bisher waren 4xx-Fehler (falsches Passwort, Validierungsfehler,
fehlende Berechtigung) im Log komplett unsichtbar - Nest loggt
standardmaessig nur unbehandelte 5xx-Fehler. Ein Login, das aus
Infra-Gruenden fehlschlaegt (siehe TLS/Cloudflare-Vorfall), war dadurch
allein aus den Logs nicht diagnostizierbar.
- AllExceptionsFilter (global): loggt jede Exception mit Methode/Pfad/
Status (4xx als warn, 5xx als error mit Stacktrace). Client-Antwort
bleibt unveraendert - HttpExceptions liefern weiter exakt ihren
bisherigen Body, unbehandelte Fehler weiter nur eine generische
500-Meldung ohne interne Details.
- requestLoggerMiddleware (global): eine Zeile pro Request (Methode,
Pfad, Status, Dauer), damit auch erfolgreiche Requests im Log
auftauchen - vorher gab's dafuer gar keine Spur.
- AuthService.login()/verifyMfaAndLogin(): konkrete Ursache pro
Fehlschlag (unbekannter User, deaktiviert, falsches Passwort,
falscher MFA-Code) zusaetzlich zum bestehenden Audit-Log-Eintrag.
Nirgends werden Passwoerter, Tokens oder Request-Bodies geloggt - nur
Methode, Pfad, Status, Dauer und die (bereits client-sichere)
Fehlermeldung.
Verifiziert: tsc/eslint clean, 304/304 Tests (7 neu), Build clean.
Bisher wurde nirgends automatisch ein erster Admin-User angelegt -
prisma/seed.ts existierte, war aber weder im Dockerfile-CMD noch sonst
irgendwo verdrahtet. Auf einer frischen Datenbank haette also niemand
sich einloggen koennen.
- Dockerfile-CMD: migrate:deploy && seed && node dist/main.js
- ts-node/typescript von devDependencies zu dependencies verschoben
(seed.ts laeuft als rohes TypeScript zur Laufzeit, kein
Kompilierschritt), tsconfig.json zusaetzlich ins Runtime-Image kopiert
- seed.ts gegen Unique-Constraint-Race bei mehreren gleichzeitig
startenden Pod-Replicas gehaertet (P2002 abgefangen statt Absturz)
- Login-Banner im Log nur beim tatsaechlichen Erst-Anlegen (URL aus
FRONTEND_ORIGIN, Benutzername, Passwort) - bewusst nicht bei jedem
Neustart, sonst wuerden veraltete/geaenderte Zugangsdaten weiter
geloggt und Credentials landen unnoetig dauerhaft in den Logs
Seed ist bereits idempotent per E-Mail-Check (auch bei deaktivierten
Usern), UsersService.assertNotLastSystemRoleUser verhindert ohnehin
das Deaktivieren des letzten aktiven Admins - kein Aenderungsbedarf
dort, siehe Konversation.
Verifiziert: tsc/eslint clean, 297/297 Tests, Build clean. Migration
und Schema live gegen die lokale DB bestaetigt (ueber WSL direkt, da
Windows->WSL-Portweiterleitung weiterhin kaputt ist); der eigentliche
Seed-Lauf selbst konnte nicht End-to-End gegen die DB verifiziert
werden (gleiches Verbindungsproblem), Banner-Ausgabe isoliert
smoke-getestet.
Benutzer koennen jetzt selbst Zwei-Faktor-Authentifizierung einrichten
und ihr Profil verwalten (Name, E-Mail, Passwort, Profilbild).
Backend:
- Neues MfaModule (src/auth/mfa/): AES-256-GCM-verschluesselte
TOTP-Secrets (otplib), QR-Code-Erzeugung, kurzlebige Setup-/
Challenge-JWTs fuer den 2-Schritt-Login-Flow
- Neues ProfileModule (src/profile/): PATCH /profile,
POST /profile/password, POST/DELETE/GET /profile/avatar,
POST /profile/mfa/enroll(/confirm), POST /profile/mfa/disable -
alle self-scoped ueber actor.id, ohne @RequirePermission (siehe
ALLOWLIST in authorization-coverage.e2e-spec.ts)
- AuthService.login() zweistufig: bei aktivem MFA erst
{ mfaRequired, mfaToken } statt Tokens, neuer Endpunkt
POST /auth/mfa/verify schliesst den Flow ab
- Admin-Recovery: POST /users/:id/mfa/reset (users:update) fuer
verlorene MFA-Geraete, da es keinen E-Mail-Reset-Flow gibt
Frontend:
- Login-Seite mit zweitem Schritt fuer den TOTP-Code
- Neue Profilseite (/profile, ausserhalb des admin-only
Settings-Bereichs): Profilbild, Name/E-Mail, Passwort, MFA-Ein-/
Ausrichtung mit QR-Code-Dialog
- Header zeigt jetzt Avatar mit Dropdown (Profil/Abmelden) statt
nur E-Mail-Text
- Admin-Users-Tabelle zeigt MFA-Status + Reset-Button
Haertung nach Security-Review (Backend/Frontend-Diff, kein CRITICAL,
mehrere MEDIUM behoben):
- Access-Token wird bei Passwortwechsel sofort ungueltig
(User.passwordChangedAt + JWT-iat-Vergleich in
PermissionResolverService.isTokenValid, vorher isUserActive) statt
erst nach bis zu 15 Minuten
- E-Mail-Aenderung verlangt jetzt aktuelles Passwort
- POST /profile/password und /profile/mfa/disable haben jetzt das
gleiche strikte Throttling wie Login (SENSITIVE_ACTION_THROTTLE)
- Kein stiller leerer Verschluesselungs-Key-Fallback mehr (wirft
jetzt explizit)
Bewusst zurueckgestellt (dokumentiert, kein aktiver Exploit-Pfad):
dedizierter Verschluesselungs-Key fuer MFA-Secrets statt
JWT_ACCESS_SECRET-Ableitung, dedizierte users:mfa-reset-Permission
statt users:update, separate JWT-Secrets pro Token-Zweck.
Verifiziert: tsc/eslint clean, 297/297 Backend- + 31/31
Frontend-Unit-Tests, Build clean auf beiden Projekten. e2e weiterhin
durch bekanntes WSL2/Postgres-Problem blockiert. Unabhaengiges
Security- und Code-Quality-Review durchgefuehrt (0 CRITICAL, 0 HIGH).
Bisher gingen "/" und unbekannte URLs immer erst zu /dashboard, das
dann bei fehlendem Login zu /login weiterleitete - unnoetiger Umweg.
Beide pruefen jetzt selbst den Auth-Status (gleiches Muster wie
(app)/layout.tsx: waehrend isLoading nichts tun, danach je nach User
direkt zu /dashboard oder /login).
- src/app/not-found.tsx: jede nicht existierende Route landet auf
/dashboard statt der Next.js-Standard-404-Seite (der bestehende
Auth-Guard in (app)/layout.tsx schickt nicht angemeldete User von
dort automatisch weiter zu /login).
- settings/layout.tsx: fehlt die Berechtigung fuer den
Einstellungen-Bereich, wird jetzt zu /dashboard umgeleitet statt nur
einen Hinweistext anzuzeigen (gleiches Redirect-Muster wie der
bestehende Login-Guard).
Login-nach-Anmeldung-Redirect zu /dashboard und der Auth-Gate in
(app)/layout.tsx bestanden bereits.
src/app/page.tsx war seit dem Projekt-Scaffold nie ersetzt worden -
die echte App laeuft komplett unter (app)/dashboard, (auth)/login etc.,
aber "/" selbst wurde nie verdrahtet und zeigte weiterhin die
create-next-app-Standardseite. / leitet jetzt zu /dashboard weiter;
(app)/layout.tsx uebernimmt von dort den bestehenden Auth-Redirect zu
/login fuer nicht angemeldete User.
Root Cause via systematic-debugging gefunden: Pod lief mit korrektem,
frisch gepushtem Image (kubectl describe bestaetigt), Ursache lag im
App-Code selbst, nicht in Build/Deploy-Pipeline.
Kein separater Kubernetes-Job mehr. Dockerfile-CMD fuehrt jetzt
`npm run migrate:deploy` aus, bevor `node dist/main.js` startet -
Tabellen werden beim ersten Verbinden automatisch angelegt. Sicher bei
mehreren gleichzeitig startenden Pod-Replicas (replicas: 2), da Prisma
migrate deploy einen DB-seitigen Advisory-Lock nutzt. deployment.yaml
bekommt eine startupProbe (failureThreshold 30 x periodSeconds 10 = bis
zu 5 Min), damit liveness/readiness den Container waehrend der
Migration nicht neu starten. ARCHITECTURE.md entsprechend aktualisiert,
migrate-job.yaml entfernt.
Bisher lief keine Migration automatisch: dem Runtime-Image fehlten
Prisma-CLI, prisma/migrations/ und prisma.config.ts (nur dist+generated
wurden kopiert, prisma war devDependency). Jetzt:
- prisma von devDependencies zu dependencies verschoben (wird fuer
migrate:deploy im --omit=dev Runtime-Image gebraucht)
- neues npm-Script migrate:deploy (prisma migrate deploy)
- Dockerfile kopiert prisma/, prisma.config.ts zusaetzlich ins Runtime-Image
- deploy/k8s/backend/migrate-job.yaml: Job, der dasselbe Backend-Image mit
ueberschriebenem command nutzt statt eines eigenen Migrations-Images,
laeuft einmalig vor dem Rollout (Reihenfolge/kubectl-Befehle als
Kommentar in der Datei)
Verifiziert: tsc, 253/253 Unit-Tests, Build clean, `npx prisma --version`
laedt prisma.config.ts korrekt.
Architektur-Entscheidung geaendert: PostgreSQL laeuft jetzt selbst im
Cluster statt auf einer bestehenden externen Instanz. Neu:
deploy/k8s/postgres/ mit StatefulSet (postgres:18-alpine,
volumeClaimTemplates, pg_isready-Probes), headless Service
(apicore-postgres) und Secret-Vorlage. Backend-DATABASE_URL zeigt auf
den neuen In-Cluster-Service. CLAUDE.md/ARCHITECTURE.md aktualisiert:
Backup-Verantwortung liegt jetzt beim Projekt, nicht mehr beim
externen Cluster-/DBA-Betrieb (Backup-CronJob als offener Punkt in
TASKS.md ergaenzt).
runs-on ubuntu-latest passte auf keinen registrierten Runner (Label
docker-builder erforderlich, siehe funktionierendes Referenzprojekt).
Push laeuft jetzt ueber den cluster-internen Service gitea:80 statt
git.encry.de, da der Cloudflare-Proxy Registry-Blob-Uploads mit "413
Payload Too Large" ablehnt. Auth ueber gitea.actor + REGISTRY_TOKEN
(GITEA_TOKEN hat kein Package-Write-Recht). Image-Pfad wird jetzt
kleingeschrieben (OCI verbietet Grossbuchstaben) - betrifft auch den
Owner-Namen in den K8s-Deployment-Manifesten.
Baut und pusht bei Push auf main sowie bei Git-Tags (vX.Y.Z) Docker-Images
fuer Backend und Frontend in die Gitea-eigene Container-Registry, zum
Deployen ins Kubernetes-Cluster. Tags: latest (main), Kurz-SHA (immer),
Versions-Tag (bei v*.*.*-Tags).
Bisher griff der isActive-Check nur auf Routen mit @RequirePermission.
Ein deaktivierter User mit gueltigem Token konnte Routen ohne
Permission-Decorator bis zum Token-Ablauf (bis zu 15 Min) weiter
erreichen. JwtAuthGuard prueft jetzt PermissionResolverService.isUserActive
auf jedem authentifizierten Request, ueber denselben 30s-Cache wie die
Permission-Aufloesung, also ohne zusaetzliche DB-Last.
Dispatchte security-reviewer und code-reviewer Agenten fuer eine
vollstaendige Pruefung des RBAC-Systems (d31aa79..HEAD). Ein
KRITISCHER Fund, zwei HIGH, ein weiteres MEDIUM, alle behoben:
KRITISCH: Deaktivieren eines Benutzers hat dessen Berechtigungen nicht
entzogen. PermissionResolverService.loadPermissions() prüfte nur den
Rollen-Status, nie User.isActive - ein bereits ausgestelltes Access-
Token behielt volle Rechte bis zum Ablauf (bis zu 15 Minuten), nicht
die vorgesehenen 30 Sekunden. UsersService rief zudem nie
invalidateUser() auf. Beides jetzt behoben; neue dedizierte
permission-resolver.service.spec.ts deckt das ab (existierte bisher
gar nicht - ein Grund, warum die Luecke unbemerkt blieb).
HIGH: Kein Schutz gegen den letzten aktiven Systemrollen-Nutzer
(ADMIN). Man konnte den letzten Admin deaktivieren oder umrollen und
sich damit komplett aussperren - ohne Selbsthilfe-Weg, da
prisma/seed.ts das Seeden ueberspringt, sobald die Admin-E-Mail
bereits existiert. UsersService.deactivate()/.update() pruefen jetzt
per assertNotLastSystemRoleUser(), analog zum bestehenden
Lockout-Schutz in RolesService fuer Systemrollen selbst.
HIGH: Der Open-Meteo-Aufruf in WeatherService hatte kein Timeout und
konnte unbegrenzt haengen. Jetzt mit AbortSignal.timeout(8000) plus
sauberem Fehler-Mapping.
MEDIUM: Trachtprognose fiel bei fehlenden Wetterdaten stillschweigend
auf "MAESSIG" zurueck statt einen eigenen Unbekannt-Zustand zu zeigen
- ein Imker haette eine echte Einschaetzung gesehen, wo keine moeglich
war. Neuer Rating-Wert UNBEKANNT in Backend und Frontend.
LOW: Veralteter Kommentar an PermissionsGuard, der noch den in Phase 3
geloeschten RolesGuard erwaehnte, korrigiert.
Verifiziert: tsc, lint, build auf beiden Projekten gruen. Backend-Unit
245/245 (17 neue Tests fuer die Fixes). Frontend-Unit 14/14. Volle
Backend-e2e-Suite konnte weiterhin nicht laufen (WSL/Postgres von
Windows aus nicht erreichbar, auch nach vollem "wsl --shutdown"-
Neustart) - jeder Fix hat aber gezielte, gruene Unit-Abdeckung.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Scannen des QR-Codes eines Volks landete bisher direkt auf einem
leeren "Neue Durchsicht"-Formular ohne jeglichen Kontext (kein
Status, keine Historie, kein Menue) - erst nach dem Speichern kam man
auf /colonies/[id], die eigentliche Uebersichtsseite mit Status,
Aktionen, Durchsicht-/Varroa-/Schwarmkontroll-/Behandlungs-/
Fuetterungshistorie.
QR fuehrt jetzt direkt zu /colonies/[id]. Der "Neue Durchsicht"-
Button ist dort weiterhin prominent vorhanden. Lastenheft Abschnitt
4.23 erlaubt explizit beide Varianten ("Volkseite beziehungsweise
eine neue Durchsicht"), diese Aenderung bleibt also spezifikations-
konform.
Verifiziert: tsc, lint, unit (228/228, inkl. der angepassten
qr.service.spec.ts-Assertion) gruen. Volle e2e-Suite konnte wegen der
wiederkehrenden WSL/Postgres-Portweiterleitungs-Unzuverlaessigkeit
nicht neu durchlaufen werden - die geaenderte Logik (ein einzelner
String) ist durch den angepassten Unit-Test direkt abgedeckt.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Neue WeatherCard auf /apiaries/[id]: Trachtprognose-Badge mit
Begruendung, 7-Tage-Vorhersagestreifen (Temperatur + Niederschlag).
Zeigt einen Hinweis statt eines Fehlers, wenn fuer den Bienenstand
noch keine Koordinaten hinterlegt sind.
TASKS.md: Wetterintegration + Trachtprognosen jetzt vollstaendig
(Backend aus vorherigem Commit + Frontend hier) abgehakt.
Verifiziert: tsc, lint, build, alle Unit-Tests (14/14) gruen. Live-
Browser-Check war blockiert durch dieselbe wiederkehrende WSL/
Postgres-Portweiterleitungs-Unzuverlaessigkeit wie zuvor in der
Session - die Funktionalitaet ist stattdessen durch die Backend-e2e-
Suite abgedeckt, die echten Postgres UND die echte Open-Meteo-API
aufruft (163/163 gruen).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Neues WeatherModule: liest Bienenstand-Koordinaten und ruft die freie
Open-Meteo-API ab (kein API-Key nötig), liefert 14 Tage Wetterhistorie
+ 7-Tage-Vorhersage sowie eine heuristische Trachtprognose (Saison +
Wetter der letzten 7 Tage + historischer Ernteschnitt für denselben
Kalendermonat am selben Standort - explizit kein ML-Modell, siehe
TASKS.md Abschnitt 26).
- GET /api/v1/apiaries/:id/weather, GET .../trachtprognose, beide
ueber die bestehende apiaries:read-Berechtigung abgesichert (kein
neuer Permission-Key nötig, gleiches Muster wie die bestehenden
Colony-Sub-Controller).
- Bienenstände ohne hinterlegte Koordinaten liefern 400 statt eines
stillen Fallbacks.
- test/weather.e2e-spec.ts ruft bewusst die echte Open-Meteo-API auf
(kostenlos, kein Auth, stabil genug für einen einzelnen Testlauf) -
validiert damit den echten Response-Vertrag statt nur ein Mock.
- TASKS.md: beide Punkte abgehakt, stale Duplikat-Zeilen im
Später-Abschnitt bereinigt.
Verifiziert: tsc, lint, build, unit (228/228), volle e2e-Suite
163/163 gruen.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Neue /settings/roles-Seite (Aktiv/Historie, Karten-Grid) zum Anlegen,
Bearbeiten, Deaktivieren und Reaktivieren von Rollen mit einer
Rechte-Matrix (nach Bereich gruppierte Checkboxen, Gruppen-Toggle,
Live-Zaehler), passend zum Backend aus Phase 1-3.
- AuthContext liest Rolle/Rechte jetzt ueber GET /auth/me statt aus
dem JWT (das seit Phase 3 keinen role-Claim mehr traegt); neue
hasPermission()-Hilfsfunktion ersetzt String-Vergleiche gegen "ADMIN".
- Sidebar zeigt Verwaltungs-Eintraege jetzt einzeln nach Berechtigung
(statt Alles-oder-Nichts fuer ADMIN), inkl. neuem "Rollen & Rechte"-
Eintrag.
- Settings-Layout blockt nur noch, wenn gar keine Verwaltungs-
Berechtigung vorhanden ist, statt stur auf die ADMIN-Rolle zu pruefen.
- Benutzerseite: Rollen-Auswahl laedt jetzt aktive Rollen von
GET /roles statt einer festen 5-Werte-Liste; sendet roleId statt
role an POST/PATCH /users.
- Totes jwt.ts (Rolle wurde bislang aus dem Token dekodiert) entfernt.
Live im Browser getestet (Login, Sidebar, Rollen anlegen/bearbeiten/
deaktivieren/reaktivieren, Benutzer-Dialog): dabei einen echten a11y-
Bug gefunden und behoben - Gruppen-Checkboxen fuer mehrwortige
Bereichsnamen ("Wiederkehrende Aufgaben", "Rollen & Rechte") hatten
keinen zugaenglichen Namen, weil die HTML-id ein Leerzeichen enthielt;
jetzt ueber einen Slug erzeugt.
Verifiziert: tsc, lint, build, alle Unit-Tests (14/14) gruen.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Letzte Phase des RBAC-Umbaus: die transitionale Rollen-Infrastruktur,
die seit Phase 1 parallel zum neuen Role/Permission-System mitlief,
ist jetzt komplett weg.
- Prisma-Migration: User.legacyRole-Spalte und LegacyRole-Enum
gedroppt (nichts liest die Spalte mehr, seit Phase 2 jeder Controller
auf @RequirePermission laeuft).
- roles.guard.ts, roles.guard.spec.ts, roles.decorator.ts geloescht;
RolesGuard aus den APP_GUARD-Providern entfernt.
- AuthenticatedUser/JWT-Payload verlieren das role-Feld (permissions
werden ohnehin frisch pro Request aus der DB aufgeloest, nicht aus
dem Claim).
- Users-API: CreateUserDto/UpdateUserDto akzeptieren jetzt roleId
(eine echte Role.id) statt der alten role-Enum-Werte; UsersService
validiert, dass die Rolle existiert und aktiv ist, bevor sie
zugewiesen wird.
- Alle 34 e2e-Fixtures und der Seed-Script folgen dem neuen Schema.
Verifiziert: tsc, lint, build, unit (220/220), volle e2e-Suite
159/159 gruen - keine uebersprungenen Tests.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Users, Webhooks, Automation, Import, AuditLog: die klassenweiten
@Roles(Role.ADMIN)-Blankets (die gefaehrlichsten der Migration, da sie
JEDE Route der Klasse abdecken) durch pro-Route @RequirePermission
ersetzt. Effektiver Zugriff unveraendert: ADMIN besitzt weiterhin alle
betroffenen Permission-Keys, jede andere Rolle bleibt ausgeschlossen.
Damit ist kein @Roles(...) mehr in der Codebase in Benutzung - alle
23 Controller laufen jetzt vollstaendig ueber @RequirePermission.
test/authorization-coverage.e2e-spec.ts (bisher it.skip mit TODO fuer
"nach Phase 2") ist jetzt scharf geschaltet und bestaetigt: 0 Routen
ohne Rechte-Pruefung, 0 ungueltige Permission-Keys.
Verifiziert: tsc, lint, build, unit (222/222), volle e2e-Suite
159/159 gruen - keine uebersprungenen Tests mehr.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Dashboard, Search, Calendar, Statistics, Qr, Export, Documents:
@Roles(...) durch @RequirePermission(...) ersetzt bzw. neu eingefuehrt
(alle diese Controller hatten zuvor gar keine Rollenpruefung -
jeder eingeloggte Nutzer konnte lesen). Documents behaelt STAFF-Zugriff
auf Upload und Loeschen.
Verifiziert: tsc, lint, volle e2e-Suite 158/159 gruen (1 getrackter Skip).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Replaces the fixed 5-value Role enum with a database-driven Role/Permission
model: admins can create custom roles with a precise per-resource/action
permission set (83-key code-owned catalog), instead of being locked to
ADMIN/BEEKEEPER/STAFF/READONLY/API_CLIENT.
- New Role/Permission/RolePermission tables; legacy Role enum renamed to
LegacyRole and kept as User.legacyRole so all existing @Roles() call
sites keep working unchanged during the migration.
- New RolesController/RolesService (CRUD, permission-set editing,
deactivate/reactivate) with lockout-prevention invariants: system roles
(ADMIN) can't be edited or deactivated, and a role still assigned to
users can't be deactivated.
- New PermissionResolverService (30s-cached, DB-fresh per user) and
PermissionsGuard + @RequirePermission(), registered as an additional
APP_GUARD alongside the legacy RolesGuard so both systems run
side-by-side until controllers are migrated in Phase 2.
- PermissionCatalogSyncService seeds any new catalog entries into the DB
on boot, so future features only touch the TS catalog.
- New GET /auth/me returns the caller's role and effective permissions.
- Migration hand-written (destructive column rename) with grants derived
from a fresh grep of every existing @Roles() call site, verified
against 5 controllers read in full for STAFF's exact write boundary.
- test/roles.e2e-spec.ts covers the new endpoints and invariants end to
end; test/authorization-coverage.e2e-spec.ts added but skipped until
Phase 2 gives every controller a @RequirePermission.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Replace the list-row layout with a responsive box grid (EntityCard):
icon avatar, title, badge, and 1-2 key fields visible at a glance per
box, matching what the user asked for ("Kaestchen" over rows).
Add an Aktiv/Historie tab split (ActiveHistoryTabs) so deactivated or
terminal-status entries move out of the default view instead of mixing
in with active ones:
- Apiaries, Colonies, Queens: split on deactivatedAt (the field the app
already uses everywhere for "Historie statt Loeschen").
- Task templates: split on the existing active boolean.
- Tasks: OPEN/IN_PROGRESS vs DONE/CANCELLED.
- Honey batches: everything vs SOLD/CONSUMED.
- Products: ACTIVE vs DISCONTINUED.
- Storage containers: everything vs OUT_OF_SERVICE.
Equipment and stock items have no lifecycle field in the schema, so
they keep the plain grid without a Historie tab.
Verified live: deactivating a Bienenstand moves it out of Aktiv into
Historie immediately, both counts update, reactivating moves it back.
New shared ListItemCard component: icon avatar, title/subtitle, trailing
badges/actions, chevron affordance when linked to a detail page. Applied
to the 9 primary data-list pages (Bienenstaende, Voelker, Koeniginnen,
Honigchargen, Aufgaben, Lager, Produkte, Inventar, Wiederkehrende
Aufgaben), replacing the plain HTML tables with the same rounded-card
list aesthetic already used elsewhere in the app (inspections,
treatments, varroa, etc.), for a consistent look across the whole
product. Admin/settings tables (Benutzer, Webhooks, Audit-Log, Import)
intentionally left as tables per user's scoping choice.
Verified each finding against the real codebase first, since the review's
diff was polluted by a stale leftover agent-worktree copy under
.claude/worktrees/ (now removed) that made several already-fixed or
never-broken issues look current (CORS, missing typecheck script, PWA
icons, font CSS, Helmet/Swagger CSP - all confirmed fine live, not
touched).
Real fixes:
- main.ts: fail-fast on missing DATABASE_URL/JWT_ACCESS_SECRET at boot,
call enableShutdownHooks() so PrismaService.onModuleDestroy() actually
runs on SIGTERM during k8s rolling deploys, PORT fallback 3000 -> 3001
to match every other place that documents the port.
- health.controller.ts: log the real underlying DB error instead of
discarding it, add a 5s timeout to the readiness query so a hung DB
fails the probe fast instead of blocking indefinitely.
- Frontend Dockerfile: declare ARG/ENV NEXT_PUBLIC_API_URL before the
build step, since Next.js inlines NEXT_PUBLIC_* vars at build time and
a runtime-only ConfigMap value never reaches the compiled bundle.
- Backend ingress.yaml: drop the no-op rewrite-target regex for a plain
prefix path (identical behavior, less confusing).
Neue PDF-Reports im Export-Modul: colony-annual-report (Durchsichten,
Behandlungen, Varroakontrollen, Schwarmkontrolle, Ernten eines Volkes fuer
ein Jahr) und apiary-annual-report (Voelker-Uebersicht + Ernten eines
Stands fuer ein Jahr). Jahr ist optional, faellt auf das aktuelle Jahr
zurueck.
Frontend: "Jahresbericht"-Button auf Volk- und Stand-Detailseite, laedt
das PDF ueber den bestehenden Blob-Download-Mechanismus herunter.
Security review findings (HIGH-1/2/3, MEDIUM):
- Rate limiting via @nestjs/throttler: global 100 req/min default, stricter
10 req/min on login and refresh. Also closes the MEDIUM finding (public
batch-lookup enumeration) since it now sits behind the global limit too.
Trust the K8s ingress's single proxy hop so throttling keys off the real
client IP, not the ingress pod's IP.
- Auth events (login success/failure, logout, token refresh) are now
audit-logged via new AuditAction values, closing the gap where the
project's own audit-log principle never actually covered the Auth module.
- Refresh-token cookie now defaults to Secure=true (opt out only via
COOKIE_SECURE=false for local HTTP dev), instead of keying off NODE_ENV
which was never set in the K8s deploy templates. Added a TLS block +
cert-manager annotation to both the backend and frontend ingress
templates, which previously had no HTTPS termination configured at all.
Neues SwarmControlCheck-Modul: strukturierte Weiselzellenkontrollen pro
Volk mit Zahl/Verdeckelungsstatus der Zellen, Schwarmstimmung, ergriffener
Maßnahme und optionalem naechsten Kontrolltermin.
Frontend: neue Karte auf der Volk-Detailseite, Erfassungsformular unter
/swarm-control/new sowie Quick-Add-Eintrag.
Neue TaskTemplate-Entität mit YEARLY/MONTHLY/INTERVAL_DAYS-Wiederholung.
Fällige Aufgaben werden lazy bei GET /tasks erzeugt (Compare-and-Swap auf
nextDueDate), damit mehrere API-Replicas im Kubernetes-Cluster dieselbe
Vorlage nicht doppelt feuern - ohne zusätzliche Cron-Abhängigkeit.
Frontend: Verwaltungsseite unter /settings/task-templates zum Anlegen,
Auflisten und Deaktivieren von Vorlagen.
Neue Funktion jenseits der Lastenheft-Checkliste, explizit im Text als
spätere Erweiterung angedacht (§4.4: "ApiCore SOLL später
Abstammungsbäume darstellen können"). Königin.motherQueenId ist eine
neue optionale Selbstreferenz (additive Migration), zusätzlich zu den
bestehenden freien Textfeldern motherLine/fatherLine – letztere
bleiben für Fälle, in denen die Mutter nicht selbst in ApiCore erfasst
ist. Die Vaterlinie bleibt bewusst Freitext, da eine Königin mit vielen
Drohnen aus unterschiedlichen Völkern begattet wird und es keine
einzelne "Vater-Königin" gibt, die referenzierbar wäre.
Backend: GET /queens/:id/lineage baut die mütterliche Ahnenkette
(rekursiv über motherQueenId, Tiefe 8) sowie den Töchterbaum
(rekursiv über die neue daughterQueens-Relation) in einer Antwort;
create/update lehnen eine nicht existierende oder auf sich selbst
zeigende motherQueenId ab.
Frontend: neue "Abstammungsbaum"-Karte auf der Königin-Detailseite
zeigt die mütterliche Linie als Liste sowie die Töchter als
eingerückten Baum, beide mit Links zu den jeweiligen Königinnen.
Anlage- und Bearbeiten-Formular bekommen ein "Mutter"-Auswahlfeld
(Bearbeiten schließt die Königin selbst aus der Liste aus).
Live in Chrome verifiziert: dreistufige Kette (Großmutter → Mutter →
Tochter) korrekt in beide Richtungen dargestellt; dabei einen echten
Anzeigefehler gefunden und behoben – das Mutter-Auswahlfeld zeigte
nach dem Speichern die rohe Datenbank-ID statt des Königin-Codes an
(derselbe SelectValue-ohne-children-Fehler wie zuvor bei der
Import-Entitätsauswahl).
Bisher liessen sich diese acht Objekttypen nur anlegen, nie nachträglich
korrigieren – Aufgaben hatten zwar schon einen PATCH-Endpunkt, aber
keine Bearbeiten-UI, die übrigen sieben hatten weder Backend- noch
Frontend-Unterstützung. Jetzt bekommt jeder Typ einen PATCH :id
Endpunkt (UpdateXDto per PartialType(PickType(...)) nach demselben
Muster wie UpdateColonyDto/UpdateApiaryDto, mit Audit-Log-Eintrag
inkl. Vorher-/Nachher-Stand) sowie eine Bearbeiten-UI:
- Aufgaben: Dialog auf der Aufgabenliste
- Durchsichten/Behandlungen/Fütterungen/Varroa-Messungen: Dialog pro
Listeneintrag auf der Volk-Detailseite
- Ernten: Dialog auf der eigenen Ernte-Detailseite
- Schleuderungen: Dialog direkt in der Rückverfolgbarkeits-Karte der
Honigcharge (keine eigene Detailseite nötig); Erntezeilen dort
verlinken jetzt zur Ernte-Detailseite statt sie zu duplizieren
- Abfüllungen: Dialog auf der Abfüllungs-Detailseite, bewusst nur
Datum/MHD/Losnummer editierbar – Glasgröße/-anzahl bleiben
unveränderlich, weil sie bereits unumkehrbar die Honigcharge-Menge
reduziert haben
Relationsstiftende Fremdschlüssel (colonyId, apiaryId/colonyIds,
harvestIds, honeyBatchId) bleiben überall bewusst nicht editierbar,
konsistent mit dem bestehenden apiaryId-Ausschluss bei UpdateColonyDto.
Neuer Endpunkt GET /api/v1/qr/:entityType/:id/link liefert den vom
QR-Dienst bereits genutzten Deep-Link als reine URL statt als SVG,
damit die Web-NFC-API (client-seitig, Chrome/Android) ihn per
NDEFReader.write() auf einen physischen NFC-Tag schreiben kann.
QrService.resolveUrl() ist aus generateSvg() herausgezogen, damit
beide Endpunkte exakt denselben Link erzeugen.
Der bestehende QrCodeDialog bekommt dadurch für alle sechs
QR-fähigen Entitäten (Völker, Bienenstände, Königinnen, Honigchargen,
Lagerbehälter, Abfüllungen) automatisch einen "Auf NFC-Tag
schreiben"-Button, der nur erscheint, wenn window.NDEFReader
tatsächlich existiert – kein Fallback für andere Browser oder iOS,
wie in der Lastenheft vorgesehen.
Neuer unauthentifizierter Endpunkt GET /api/v1/public/batches/:lot
(Abschnitt 27 der Lastenheft) liefert kontrolliert freigegebene
Rückverfolgbarkeitsdaten einer Abfüllcharge – Produkt, Erntemonat,
Bienenstand-Namen, Schleuderdatum, Wassergehalt – ohne interne IDs,
Adressen oder Koordinaten preiszugeben. Die neue Seite /herkunft/{lot}
liegt außerhalb der (app)-Route-Group und damit ohne Login/Sidebar,
gedacht als Ziel eines QR-Codes auf dem Honigglas. Der bestehende
QR-Dienst kennt jetzt den Entity-Typ "bottling-batches" und verlinkt
intern auf diese neue öffentliche Seite; ein entsprechender
QR-Code-Button samt Klartext-Link steht ab sofort auf der
Abfüllungs-Detailseite bereit.
Service Worker (public/sw.js) cached die App-Shell für Seiten-GETs
(network-first, cache-fallback). Durchsichten, die offline gespeichert
werden, landen in einer IndexedDB-Warteschlange (idb) statt zu
scheitern und werden automatisch synchronisiert, sobald wieder
Netzwerk verfügbar ist (Mount + online-Event), mit manuellem Retry
über ein Badge im App-Header. Ein modulweiter In-Flight-Lock in
sync.ts verhindert doppelte Synchronisierung bei gleichzeitigen
Aufrufen (gefunden über React-StrictMode-Doppel-Mount, das ohne den
Lock dieselbe Durchsicht zweimal anlegte).
Neue Seite /apiaries/map zeigt alle Bienenstände mit hinterlegten
Koordinaten auf einer Leaflet-Karte (OpenStreetMap-Kacheln), mit
Marker-Popup, das zum jeweiligen Bienenstand verlinkt. Karte wird
per next/dynamic mit ssr:false geladen, da Leaflet auf window/document
zugreift.
Breiten-/Längengrad waren zwar seit dem MVP im Backend-DTO vorhanden,
aber über kein Formular erreichbar – ohne diese Lücke wäre die Karte
dauerhaft leer geblieben. Daher Breitengrad-/Längengrad-Felder auf der
Bienenstand-Anlegen- und Bearbeiten-Seite ergänzt.
Marker-Icons werden aus public/leaflet/ bedient statt aus
node_modules-Bild-Imports, da Turbopack Bilder aus node_modules nicht
als StaticImageData auflöst (führte zu "iconUrl not set"-Laufzeitfehler,
live im Browser gefunden und behoben) – der von Leaflet empfohlene
Workaround für Bundler-Icon-Probleme.
Live verifiziert: Koordinaten speichern, Karte zeigt Marker an
korrekter Position, Popup verlinkt korrekt, Empty State bei fehlenden
Koordinaten, mobile Breite ohne Overflow. Zero Konsolenfehler.
Backend und Frontend beide fertig, getestet und live verifiziert
(siehe die beiden vorherigen Commits). Gesamter Backend- und
Frontend-Build war während der gesamten v1.x-Arbeit durchgehend grün.
Offen bleibt nur noch die abschließende End-to-End-Verifikation der
erweiterten Kernkette (Stand -> Volk -> ... -> Produkt) als
zusammenhängender manueller Durchlauf.
Neue Seite /settings/import (nur Administrator, Sidebar-Link unter
Verwaltung): Datentyp wählen (Bienenstände/Völker/Königinnen), CSV-Datei
hochladen, danach Ergebnisbericht mit Zusammenfassung (erstellt/
übersprungen/Fehler) und Zeile-für-Zeile-Tabelle inkl. Statusbadge und
Fehlermeldung.
Live gegen das Backend verifiziert: Upload einer CSV mit einer gültigen
und einer ungültigen Zeile erzeugt korrekt 1 erstellt / 1 Fehler mit
Validierungsmeldung, der neu angelegte Bienenstand erscheint danach in
der regulären Bienenstände-Liste. Zero Konsolenfehler.
Neues ImportModule mit einem abhängigkeitsfreien CSV-Parser
(src/import/csv.ts, RFC4180-artig: Quoting, escapte Anführungszeichen,
CRLF/LF) passend zum bereits im ExportModule verwendeten
Schreibformat, sodass exportierte CSVs unverändert wieder importierbar
sind.
Endpunkt: POST /import/:entity (multipart, nur Administrator) für
apiaries, colonies, queens. Jede Zeile wird über die exakt gleichen
class-validator-DTOs geprüft, die auch die regulären POST-Endpunkte
nutzen (CreateApiaryDto/CreateColonyDto/CreateQueenDto) – keine
duplizierte Validierungslogik.
Ergebnisbericht pro Zeile (erstellt/übersprungen/Fehler + Meldung):
- Colonies/Queens: Zeilen mit bereits existierendem Code werden
übersprungen statt einen Fehler zu werfen (idempotenter Re-Import)
- Colonies lösen den Bienenstand über eine "apiaryName"-Spalte auf
(case-insensitive), da eine CSV keine internen IDs kennen kann;
unbekannter Name → Fehlerzeile statt Abbruch des gesamten Imports
Unit-Tests für den CSV-Parser (Quoting, escapte Anführungszeichen,
CRLF, leere Zeilen) und den ImportService (alle drei Entitäten,
Validierungsfehler, Duplikat-Skip, unbekannter Bienenstand). E2E-Tests
über echten multipart-Upload (Rollenprüfung, Dateiendungsprüfung, alle
drei Entitäten, Re-Import-Idempotenz).
Neue Seite /settings/automation-rules (nur Administrator, Sidebar-Link
unter Verwaltung): zeigt alle drei fest hinterlegten Regeln mit
Beschreibung, Aktiv/Inaktiv-Umschalter, editierbarem Aufgabentext und
– nur bei Varroa-Schwellenwert – editierbarem Schwellenwert.
Zusätzlich zwei Lücken im Durchsicht-Formular geschlossen, ohne die
wären zwei der drei Trigger über die Oberfläche nie auslösbar gewesen:
- "Stifte vorhanden"-Checkbox ergänzt (eggsPresent wurde vom Formular
bisher gar nicht gesendet, obwohl Backend und Typ es unterstützen)
- "Futtervorrat ausreichend"-Checkbox durch ein Select mit
ausreichend/niedrig/kritisch ersetzt (bisher nur binär, "kritisch"
war nicht erreichbar)
Live gegen das Backend verifiziert: Speichern einer Durchsicht mit
Königin nicht gesehen + keine Stifte erzeugt automatisch die erwartete
Aufgabe (sichtbar unter /tasks) und eine Benachrichtigung, zero
Konsolenfehler. Aktiv/Inaktiv-Umschalter auf der Automatisierungen-Seite
live getestet.
Neues AutomationModule mit AutomationRule-Modell (ein Row je Trigger-Typ,
per Unique Constraint erzwungen). Drei fest im Code hinterlegte Trigger,
die Admins nur aktivieren/deaktivieren bzw. deren Konfiguration/Aufgaben-
text anpassen können:
- QUEENLESS_NO_EGGS: Königin nicht gesehen UND keine Stifte vorhanden
(bei Durchsicht) → Kontrollaufgabe
- VARROA_THRESHOLD: Varroa-Schwellenwert überschritten (konfigurierbar,
Standard 10 Milben/Tag) → Behandlungsprüfung
- FEED_STATUS_CRITICAL: Futterstatus "kritisch" (bei Durchsicht) →
Fütterungsaufgabe
Jeder ausgelöste Trigger erzeugt eine Task UND eine Notification sowie
einen Audit-Log-Eintrag; Tasks lösen zusätzlich das bestehende
task.created-Webhook aus. InspectionsService und VarroaService rufen
die Auswertung direkt nach dem jeweiligen Erstellen auf.
Neue Endpunkte (nur Administrator): GET/PATCH /automation-rules.
Unit-Tests für AutomationService (alle drei Trigger, Deaktivierung,
Update mit Audit-Log) sowie E2E-Tests (Rollen-Check, alle drei Trigger
end-to-end über echte Inspection-/Varroa-Endpunkte, Deaktivierung
verhindert weitere Trigger). E2E-Suite zweimal hintereinander grün
verifiziert (Idempotenz der globalen Regel-Zeilen sichergestellt).
Migration auf Dev- und Test-Datenbank angewendet.
Neue HoneyBatchStorageCard-Komponente auf der Honigcharge-Detailseite:
zeigt den aktuell zugeordneten Lagerbehälter, einen "Umlagern"-Dialog
(Ziel-Lagerbehälter wählen, optionale Notiz) und die vollständige
Umlagerungshistorie (von/nach, Zeitpunkt, Notiz) im etablierten
EmptyState-Stil.
HoneyBatch-Typ um storageContainer erweitert, neue
HoneyBatchRelocation/RelocateHoneyBatchInput-Typen und passende
API-Funktionen ergänzt. Live gegen das Backend verifiziert: Umlagerung
zwischen zwei echten Lagerbehältern aktualisiert die Anzeige und die
Historie korrekt, zero Konsolenfehler.
Neues HoneyBatchRelocation-Modell dokumentiert jede Umlagerung einer
Honigcharge zwischen Lagerbehältern (von/nach, Zeitpunkt, Benutzer,
Notiz). HoneyBatchesService.relocate() aktualisiert
HoneyBatch.storageContainerId transaktional zusammen mit dem
Historieneintrag; wirft NotFoundException bei unbekanntem Zielbehälter.
Neue Endpunkte: POST /honey-batches/:id/relocate,
GET /honey-batches/:id/relocations. honeyBatchInclude liefert jetzt
zusätzlich den aktuell zugeordneten storageContainer mit.
Unit- und E2E-Tests ergänzt (Mehrfachumlagerung, Historienreihenfolge,
404 bei unbekanntem Behälter). Migration auf Dev- und Test-Datenbank
angewendet.
Backend war bereits fertig, Frontend-Upload-Widget wurde soeben ergänzt
und live verifiziert (siehe vorheriger Commit). Anmerkung ergänzt, dass
Durchsicht keine eigene Detailseite hat und daher aktuell nicht
angebunden ist.
Backend war bereits fertig, das Frontend fehlte bislang. Neue
gemeinsame DocumentUploadCard-Komponente (Liste hochgeladener Dateien,
Upload per Dateiauswahl, Download, Löschen mit Bestätigungsdialog da
unumkehrbar) plus dazugehöriges src/lib/documents/ API-Modul.
apiFetch erzwingt bislang immer Content-Type: application/json, was
multipart-Uploads verhindert hätte – daher neue apiFetchFormData-
Hilfsfunktion in api-client.ts ergänzt (kein manuell gesetzter
Content-Type, damit der Browser die multipart-Boundary selbst setzt).
Eingebunden auf allen Detailseiten, die laut TASKS.md Modul 29 vorsehen
und tatsächlich eine eigene Route haben: Bienenstand, Volk, Königin,
Honigcharge. Durchsicht hat keine eigene Detailseite (nur Einträge in
der Volk-Unterliste) und wird daher vorerst ausgelassen.
Live verifiziert: Upload, Download und Löschen (inkl. Bestätigungsdialog)
funktionieren End-to-End gegen das Backend, zero Konsolenfehler auf
allen vier Detailseiten.
Header von BottlingBatchDetailPage und HarvestDetailPage um Icon-Badge
(Package bzw. Boxes im Honig-Gradient) ergänzt, konsistent zum bereits
überarbeiteten Look aller anderen Detailseiten. Schließt die letzten
beiden verbliebenen Detailseiten ohne Icon-Badge ab.
Ersetzt den bisherigen reinen Fließtext ("Noch keine X dokumentiert.")
durch die gemeinsame EmptyState-Komponente (Icon, Titel, Beschreibung)
in allen verschachtelten Listen auf den Volk- und Königin-Detailseiten:
Fütterung, Behandlungen, Varroa, Durchsichten, Historie (Volk und
Königin), Bewertungen und Honigmessungen.
Rundet den bereits umgesetzten EmptyState-Look der Hauptlistenseiten
konsistent auf die Detailseiten-Unterkarten ab.
Ersetzt die bisherige schlichte CardTitle-Überschrift durch die
gemeinsame PageHeader-Komponente (Icon-Badge im passenden Gradient,
Titel, kurze Beschreibung) auf allen Formularseiten zum Anlegen neuer
Datensätze (Bienenstände, Völker, Königinnen, Aufgaben, Produkte,
Inventar, Schleuderungen, Abfüllungen, Durchsichten, Varroamessungen,
Behandlungen, Fütterungen, Ernten).
Konsistent zum bereits überarbeiteten Look der Listen- und Detailseiten;
die Beschreibungszeile gibt zusätzlich beim Öffnen sofort Kontext, was
die Seite tut – hilfreich für Nutzer, die schnell zwischen vielen
Formularen wechseln.
Neue gemeinsame EmptyState-Komponente (Icon in Kreis, Titel, Beschreibung,
gestrichelter Rahmen) und Einbindung auf allen 9 Listenseiten mit Tabellen
(Völker, Bienenstände, Königinnen, Aufgaben, Honigchargen, Benutzer,
Inventar, Produkte, Lager mit Behältern und Artikeln).
Bug behoben: Bei leerem Datensatz wurde bisher nur eine Tabelle mit
Kopfzeile und keinerlei Inhalt oder Erklärung angezeigt – wirkte wie ein
Ladefehler statt eines normalen "noch keine Daten"-Zustands. Besonders
für neurodivergente Nutzer wichtig, da Unklarheit über den Systemzustand
("ist das kaputt oder einfach leer?") unnötige kognitive Last erzeugt.
Header von HoneyBatchDetailPage und EquipmentDetailPage um Icon-Badge
(Droplet im Honig-Gradient bzw. Wrench im Himmel-Gradient) ergänzt,
konsistent zum bereits überarbeiteten Look der übrigen Detailseiten.
Header von ColonyEditCard, ApiaryEditCard und QueenEditCard um Icon-Badge
im Honig-/Salbei-Gradient ergänzt, passend zum bereits überarbeiteten
Look der Listen- und Übersichtsseiten.
Zusätzlich Bug behoben: ColonyEditCard und QueenEditCard nutzten bislang
ein hartcodiertes ACTIVE-Ternary statt der gemeinsamen statusTone()-Hilfsfunktion,
wodurch z. B. WEAK/CONSPICUOUS/DISSOLVED-Völker und LOST/DECEASED-Königinnen
auf der eigenen Detailseite nur als neutrales "secondary"-Badge statt in
Warn-/Fehlerfarbe erschienen.
Inventar, Produkte, Lager, Kalender, Suche und Audit-Log bekommen den
gleichen Gradient-Icon-Header wie die uebrigen Seiten. Lager-Seite hatte
zwei h1-artige Ueberschriften fuer zwei Abschnitte (Lagerbehaelter/
Lagerartikel) auf einer Seite; jetzt ein PageHeader fuer die Seite plus
gleich grosse Unterueberschriften fuer beide Abschnitte statt
uneinheitlicher Groessen.
Aktiver Tab war kaum vom Tab-Leisten-Hintergrund zu unterscheiden
(fast gleicher Ton). Jetzt weisser Karten-Hintergrund mit Schatten
und Primaerfarben-Text fuer den aktiven Tab, groesserer Radius/Padding
passend zum Rest der Oberflaeche.
Neue PageHeader-Komponente (Gradient-Icon-Badge + Titel + Beschreibung
+ Aktionen) ersetzt die nackten <h1>-Zeilen auf den meistgenutzten
Seiten (Voelker, Bienenstaende, Koeniginnen, Aufgaben, Honigchargen,
Statistik, Webhooks, Benutzerverwaltung). Gradient variiert pro
Bereich (Honig/Salbei/Himmel), passend zum jeweiligen Sidebar-Icon.
Primaerer Button-Stil nutzt jetzt den Honig-Gradient statt Volltonfarbe,
mit Schatten und Hover-Lift. Buttons/Inputs/Select/Textarea/Dialog auf
rounded-xl bzw. rounded-2xl vereinheitlicht, Formularfelder mit Karten-
hintergrund, dezentem Schatten und Hover-Rahmen statt nacktem Rand.
Dialog-Footer-Radius/Aussenabstand an das groessere Popup-Padding
angepasst, sonst waere die Ecke am Fusszeilen-Rand versetzt gewesen.
Die geteilte Table-Komponente (11 Listenseiten: Voelker, Bienenstaende,
Koeniginnen, Aufgaben, Honigchargen, Inventar, Produkte, Lager, Benutzer,
Audit-Log, Webhooks) bekommt jetzt denselben Karten-Rahmen/Schatten wie
der Rest der Oberflaeche statt nackt auf dem Hintergrund zu schweben,
plus getoente Kopfzeile, ruhigere Uppercase-Spaltentitel und mehr
Zeilenabstand.
Kartenhintergrund und Seitenhintergrund waren fast farbgleich, wodurch
alles flach wirkte trotz vorhandenem Schatten. Staerkerer Kontrast
(--background/--card/--muted/--border), neue Gradient- und
Schatten-Tokens (--gradient-honey/-sage/-sky, --shadow-soft/-elevated).
Card-Komponente: rounded-2xl, echter sichtbarer Schatten, optionaler
Hover-Lift (interactive-Prop). Sidebar: solide Gradient-Badge, kraeftiger
Active-Pill-Zustand statt blassem Tint. Header: sticky mit Blur-Schatten
statt hartem Rand. Dashboard: Gradient-Hero mit Begruessung und
Honeycomb-Muster, farbige Gradient-Icon-Badges pro KPI-Kachel,
Hinweise-Zeilen mit Icon statt rohem Enum-Badge (dabei toten
CONSPICUOUS_COLONY-Style-Key gefunden und auf COLONY_ATTENTION
korrigiert). Honeycomb-Hintergrund aus der Login-Seite in eine
gemeinsame Komponente extrahiert und dort mit dem gleichen
Gradient-Badge-Header konsistent gemacht.
Volk aufloesen, Bienenstand deaktivieren, Benutzer deaktivieren und
Webhook loeschen liefen bisher ohne jede Rueckfrage direkt auf Klick.
Neue wiederverwendbare ConfirmDialog-Komponente erzwingt jetzt eine
bewusste Bestaetigung. Zusaetzlich zeigen Volk/Bienenstand/Benutzer
nach dem Aufloesen/Deaktivieren einen Reaktivieren-Button (nutzt die
neuen Backend-Endpunkte), da diese Aktionen nie ein echtes Loeschen
sind und ein versehentlicher Klick bisher nicht rueckgaengig zu
machen war.
Symmetrisch zu Modul-27/28-Fix fuer Colony/Apiary: kein Weg, einen
versehentlich deaktivierten Benutzer wieder zu aktivieren. Neuer
POST :id/activate-Endpunkt setzt isActive zurueck auf true und
loescht deactivatedAt; ConflictException wenn bereits aktiv.
Da fachliche Objekte nie hart geloescht werden (nur status +
deactivatedAt), fehlte bisher ein Weg, ein versehentlich aufgeloestes
Volk oder deaktivierten Bienenstand wieder zu aktivieren. Neue
POST :id/reactivate-Endpunkte setzen status zurueck auf ACTIVE und
loeschen deactivatedAt; ConflictException wenn bereits aktiv.
/settings/webhooks: Liste, Anlage-Dialog mit Ereignis-Auswahl und
einmaliger Secret-Anzeige (Kopieren), Loeschen. Sidebar-Eintrag unter
Verwaltung. Live in Chrome verifiziert (Anlegen/Kopieren/Loeschen,
SSRF-Ablehnung, Mobile-Breakpoint); dabei einen Reset-Bug im
Anlage-Dialog gefunden und behoben (Fertig-Button loeschte den
Dialogzustand nicht, da onOpenChange bei programmatischem Schliessen
nicht feuert).
1. The URL guard only classified IP-literal hostnames; any plain domain
name (the common case) skipped validation entirely. Hostnames are now
resolved via DNS and every returned address is checked against the
same private/loopback/link-local ranges (net.BlockList), so an
attacker-controlled domain pointed at an internal IP is rejected too.
2. fetch() follows redirects by default, so a registered public URL could
302 to an internal target at delivery time. Delivery now uses
redirect: 'manual' so such a redirect is never followed.
WebhooksService.create() rejected obviously private, loopback, or
link-local URLs (incl. cloud metadata 169.254.169.254 and the
decimal/octal/hex IPv4 shorthand encodings the WHATWG URL parser
normalizes) so an admin-configured webhook can no longer be used to
proxy requests into internal infrastructure. Registration-time only,
no DNS resolution (DNS-rebinding is out of scope for this check).
WebhooksModule (WebhookSubscription: URL, Events, Secret, aktiv) mit
Event-Dispatcher, angehaengt an 10 Ereignisse in 8 bestehenden Services
(colony.created, inspection.created, queen.created, queen.replaced,
harvest.created, batch.created, batch.updated, product.stock_changed,
task.created, task.completed). Auslieferung ist best-effort und blockiert
die ausloesende Anfrage nicht bei Fehlern.
Tab-basierte /statistics-Seite (Voelker, Koeniginnen, Bienenstaende,
Varroa, Honig, Futter) mit animierten Balkendiagrammen, Sidebar-Eintrag
und Live-Verifikation ueber alle Tabs sowie Mobile-Breakpoint.
Sonner-Toaster global eingebunden. Erfolgs-Toast auf allen sechs
Schnellerfassungs-Formularen (Durchsicht, Varroa, Behandlung,
Fuetterung, Aufgabe, Ernte) - ohne Toast blieb nach dem Speichern
und Wegnavigieren keine sichtbare Bestaetigung, dass die Eingabe
gespeichert wurde. Wichtig fuer klares Feedback bei zeitkritischer
Erfassung am Bienenstand.
Ersetzt die horizontale Kopfzeilen-Navigation durch eine feste
Sidebar links: gruppiert nach Betrieb / Arbeit / Ernte & Lager /
Verwaltung mit Icon+Label je Eintrag, um kognitive Last zu
reduzieren. Aktiver Zustand klar farblich hervorgehoben. Mobile
Ansicht (Bottom-Nav + FAB) bleibt unveraendert bestehen.
Sanfte, kurze Seitenuebergangsanimation (PageTransition, respektiert
prefers-reduced-motion ueber motion-safe:) fuer Orientierung ohne
Ablenkung.
Ausserdem: wiederkehrenden Anzeigefehler in Status-Filtern behoben
(Select zeigte den rohen Sentinel-Wert "ALL" statt "Alle Status" an)
auf Voelker-, Honigchargen-, Aufgaben-, Koeniginnen- und
Bienenstand-Listen. Live in Desktop-, Tablet- und Mobilbreite
verifiziert, kein horizontales Overflow.
Bewertungsformular (Volksstaerke, Sanftmut, Wabensitz, Schwarm-
trieb, Brutbild, Honigertrag je 1-5) plus Verlauf auf der
Koenigin-Detailseite. Live verifiziert.
QueenEvaluation-Modell (Volksstaerke, Sanftmut, Wabensitz, Schwarm-
trieb, Brutbild, Honigertrag je Skala 1-5) als Sub-Ressource der
Queens-API: GET/POST /queens/:id/evaluations. Vorgezogen vor Modul
27 (Statistik), da dessen Koeniginnen-Auswertungen auf diesen Daten
aufbauen.
NotificationBell in der Kopfzeile: ungelesene Anzahl als Badge,
Dropdown mit Prioritaets-Badges (INFO/HINWEIS/WARNUNG/KRITISCH),
Klick markiert als gelesen und fuehrt zum verknuepften Objekt.
Ausserdem IDOR im Backend gefunden und behoben (siehe vorheriger
Commit) - live mit einem echten Datensatz verifiziert.
markRead() pruefte bisher nicht, ob die Benachrichtigung dem
anfragenden Nutzer gehoert - jeder authentifizierte Nutzer konnte
fremde Benachrichtigungen als gelesen markieren. Jetzt: 403 bei
fremder Benachrichtigung, weiterhin erlaubt fuer eigene und
Broadcast-Benachrichtigungen (userId = null). E2E-Regressionstest
ergaenzt.
NotificationsModule: Notification-Modell mit Prioritaeten INFO/
HINWEIS/WARNUNG/KRITISCH, GET /notifications (eigene + Broadcast),
POST /notifications/:id/read. record() als interne API fuer
kuenftige Automatisierungsregeln (Modul 30) vorgesehen.
/calendar Monatsansicht mit Voelkerdaten aus Durchsichten, Aufgaben,
Behandlungen, Fuetterungen, Ernten und Koeniginnen-Zuweisungen;
Bienenstand-Filter, Monatsnavigation, Klick auf Ereignis fuehrt zum
verknuepften Objekt. Nav um Kalender-Link ergaenzt.
CalendarModule mit GET /calendar?from=&to=&apiaryId= aggregiert
Durchsichten, Aufgaben, Behandlungen, Fuetterungen, Ernten und
Koeniginnen-Zuweisungen zu einem einheitlichen Ereignisfeed. Rein
lesende Aggregation, kein neues Datenmodell.
/equipment, /equipment/new, /equipment/:id mit Zustandsaenderung.
statusTone()-Helper um Equipment-Zustaende (NEW/GOOD/WORN/DEFECTIVE)
erweitert, Nav-Dropdown um Inventar ergaenzt.
/products, /products/new, /bottling/new, /bottling/:id. Verbindung
von der Honigcharge-Detailseite zur Abfuellung, Nav-Dropdown "Mehr"
fuer alle Rollen mit Produkte/Abfuellungen erweitert.
ProductsModule (GET/POST /products, GET/PATCH /products/:id) und
BottlingModule (GET/POST /bottling-batches) mit transaktionaler
Reduzierung der HoneyBatch-Restmenge beim Abfuellen; Ablehnung bei
Abfuellmenge > verfuegbarer Menge. Neue Prisma-Modelle Product und
BottlingBatch (Migration bottling_products).
Wiederverwendbarer QrCodeDialog (authentifizierter Blob-Fetch)
auf Volk-, Bienenstand-, Koenigin-, Honigcharge-Detailseiten und
je Lagerbehaelter-Zeile eingebunden.
QrModule mit GET /qr/:entityType/:id.svg fuer colonies (Deep-Link
direkt zu /inspections/new), apiaries, queens, honey-batches und
storage-containers. Nutzt pdfkit-Nachbar qrcode zur SVG-Erzeugung.
Module 22-33 (SOLL) mit Backend/Endpunkte/Frontend/Checkbox-Struktur
analog zum MVP spezifiziert. Spaeter-Liste (KANN) in software-seitig
umsetzbare Punkte und explizit nicht umsetzbare (native App, ML,
Sprach-/Sensorintegration) mit Begruendung aufgeteilt.
CSV-Export in den Listen-Views (Voelker, Honigchargen) und
PDF-Rueckverfolgbarkeitsbericht auf der Honigcharge-Detailseite.
apiFetchBlob() im API-Client ergaenzt fuer authentifizierte
Blob-Downloads (JWT im Authorization-Header statt Cookie).
ExportModule mit GET /export/colonies.csv, /export/honey-batches.csv,
/export/:entity.json (apiaries/colonies/queens/honey-batches/tasks)
und /export/:report.pdf (honey-batch-traceability via pdfkit).
Ausserdem fehlendes "typecheck"-Skript in package.json ergaenzt
(von CLAUDE.md dokumentiert, war aber nicht vorhanden).
Migrations-, Backup- und Restore-Vorgehen in ARCHITECTURE.md
Abschnitt 8 festgehalten. migrate deploy/dev bereits einziger
Migrationsweg (kein db push), /health/ready prueft DB bereits.
AuditModule um GET /audit-log?entityType=&entityId= erweitert
(nur Administrator). Zusaetzlich Jest-E2E-Parallelitaet begrenzt
(maxWorkers: 4), um wiederkehrende Verbindungsengpaesse gegen
Postgres unter vielen parallelen Testsuiten zu vermeiden.
Verifiziert: alle 73 Endpunkte über 17 Tags in /api/v1/docs
sichtbar; jeder Mutations-Endpunkt trägt @Roles (oder @Public bei
Login/Refresh/Logout), lesende Endpunkte sind für alle
authentifizierten Rollen offen (RolesGuard-Standardverhalten).
Mobile Bottom-Navigation und permanent erreichbare "+ Neu"-
Schnellaktion (Durchsicht/Varroa/Behandlung/Fütterung/Aufgabe/Ernte).
Header-Navigation überarbeitet, damit sie bei schmaler Breite intern
scrollt statt die Seite horizontal zu sprengen. Verifiziert bei
Smartphone- (390px) und Tablet-Breite (820px).
/search-Seite über Bienenstände, Völker, Königinnen, Aufgaben und
Honigchargen. Filterleisten (Status/Standort) in den Listen-Views
für Bienenstände, Völker, Königinnen und Honigchargen.
SearchModule mit GET /search?q= über Bienenstände, Völker,
Königinnen, Aufgaben und Honigchargen. Kombinierbare Filter
(Status, Standort, Bienenstand) in den bestehenden List-Endpunkten.
/dashboard als Startseite nach Login mit Kennzahlen-Kacheln und
Hinweisen (überfällige Aufgaben, auffällige Völker, Lagerartikel
unter Mindestbestand).
StorageModule mit Lagerbehaeltern, Lagerartikeln und
Bestandsbewegungen (Zugang/Abgang/Umlagerung/Korrektur), die den
Artikelbestand automatisch fortschreiben.
HoneyBatchesModule inkl. Honigmessungen und
Rückverfolgbarkeits-Endpunkt, der eine Charge über die Schleuderung
und Ernte bis zu den Ursprungsvölkern zurückverfolgt.
/harvests/new (Bienenstand + Mehrfachauswahl Völker), /harvests/[id]
und /extractions/new (Mehrfachauswahl Ernten). Bienenstand- und
Ernte-Detailseiten verlinken direkt in die Kette.
HarvestsModule und ExtractionsModule als Fundament der
Rückverfolgbarkeitskette: eine Ernte kann mehrere Völker umfassen,
eine Schleuderung mehrere Ernten zusammenführen.
/queens: Liste; /queens/new: Anlegeformular; /queens/[id]: Bearbeiten
(Zucht/Herkunft/Status/Notizen), "Volk zuweisen"-Dialog (nur ACTIVE
Voelker zur Auswahl) und Volk-Historie. Bearbeitungsformular initialisiert
Zustand direkt aus der geladenen Queen (gleiches Key-Reset-Pattern wie
bei Apiaries/Colonies).
Nav-Leiste und proxy.ts-Matcher um /queens erweitert.
10 Vitest-Tests, Lint, Typecheck und Build gruen. Kompletter Flow
(Anlegen, Volk zuweisen, Historie) live im Browser gegen den echten
Server verifiziert.
QueensModule: CRUD + assign-colony + history, entsprechend Lastenheft
§4.5 (Koeniginnenverwaltung). assign-colony schliesst die offene
QueenColonyHistory der bisherigen Koenigin am Zielvolk (falls vorhanden)
sowie eine evtl. noch offene History dieser Koenigin an einem anderen
Volk, bevor Colony.currentQueenId gesetzt und ein neuer History-Eintrag
angelegt wird - damit bleibt jeder Koeniginnenwechsel nachvollziehbar,
wie gefordert. Rollen wie bei Apiaries/Colonies: Lesen offen, Schreiben
auf ADMIN/BEEKEEPER beschraenkt.
5 Unit-Tests + 2 e2e-Tests, Lint, Typecheck und Build gruen.
/colonies: Liste; /colonies/new: Anlegeformular mit Bienenstand-/
Volkstyp-Auswahl; /colonies/[id]: Bearbeiten, Aktionen (Umsetzen,
Ableger bilden, Vereinigen, Aufloesen je als Dialog/Button) und
Historie-Anzeige. Aktionen deaktiviert, sobald das Volk nicht mehr
ACTIVE ist (Frontend-Pendant zum neuen Backend-Guard).
Beim Live-Testen im Browser gefunden und behoben:
- Echter Root-Cause fuer den fruehren Session-Abbruch nach Reload: der
Refresh-Token ist einmalig/rotierend, und React StrictMode ruft den
Session-Restore-Effect im Dev-Modus doppelt auf. Der vorherige
AbortController-Fix verhinderte nur, dass der Client die verworfene
Antwort nutzt - er verhinderte NICHT, dass die zweite Anfrage bereits
serverseitig verarbeitet wurde, bevor abort() griff. Fix: echtes
Request-Deduplizieren in refreshRequest() (gemeinsames In-Flight-
Promise) statt Abbruchversuch - der Standardansatz fuer rotierende
Refresh-Token.
- Historie-Text beim Vereinigen war fuer die aufnehmende Seite falsch
("Entstanden durch Vereinigung" impliziert Neuanlage) - jetzt
"Anderes Volk eingegliedert".
Nav-Leiste und proxy.ts-Matcher um /colonies erweitert.
9 Vitest-Tests, Lint, Typecheck und Build gruen. Kompletter Flow
(Anlegen, Umsetzen, Ableger bilden, Vereinigen, Aufloesen, Historie)
live im Browser gegen den echten Server verifiziert, inkl. Rollen-
filterung in den Aktions-Dialogen.
Beim Live-Test im Browser gefunden: move/split/merge liessen sich auch
auf einem bereits DISSOLVED/MERGED-Volk aufrufen und wuerden dort
klaglos durchlaufen - ein Datenintegritaetsproblem, kein reines
UI-Detail. assertActive() wirft jetzt ConflictException (409), wenn
das betroffene Volk (bzw. bei merge: Ziel- und alle zu vereinigenden
Voelker) nicht ACTIVE ist.
Neuer Unit-Test + neuer e2e-Test (dissolve, danach move -> 409).
/apiaries: Liste mit Status-Badge; /apiaries/new: Anlegen-Formular;
/apiaries/[id]: Bearbeiten (Name/Standort/Adresse/Notizen) + Deaktivieren.
Editierbares Formular liest den Ausgangszustand direkt aus der geladenen
Apiary (kein useEffect+setState-Sync, React-Key-Reset-Pattern statt
Effect - vermeidet kaskadierende Renders).
(app)/layout.tsx: einfache Nav-Leiste (Bienenstände fuer alle, Benutzer
nur fuer Admins) - Platzhalter bis zur echten Bottom-Nav/Sidebar aus
Modul 15. proxy.ts-Matcher um /apiaries/* erweitert.
Live im Browser gefunden+behoben: Base-UI-Button meldete fehlende
native Button-Semantik beim Rendern als Link (`nativeButton={false}`
noetig, wenn Button auf ein <a> zeigt).
8 Vitest-Tests, Lint, Typecheck und Build gruen. Kompletter Flow
(Anlegen, Liste, Bearbeiten, Deaktivieren, Navigation) live verifiziert.
/login: Formular gegen AuthProvider (Access-Token im Memory, Refresh
per httpOnly-Cookie), Fehlermeldung bei falschen Zugangsdaten.
(app)/layout.tsx: geschuetztes Layout, leitet ohne Session zu /login um,
Header mit Nutzer-E-Mail + Abmelden. settings/layout.tsx: zusaetzliches
Admin-Gate fuer /settings/users.
/settings/users: Liste, Anlegen-Dialog (shadcn/Base UI Dialog+Select),
Deaktivieren - alles ueber TanStack Query gegen die Users-API.
proxy.ts (Next 16 Nachfolger von middleware.ts): optimistische
Cookie-Praesenz-Pruefung fuer /login und /settings/*, echte
Autorisierung bleibt serverseitig (RolesGuard).
Backend-Fixes waehrend Browser-Verifikation gefunden und behoben:
- CORS fehlte komplett (app.enableCors, FRONTEND_ORIGIN) - jeder
Cross-Origin-Request von :3000 nach :3001 wurde geblockt.
- React-StrictMode-Doppel-Effect liess zwei parallele Refresh-Aufrufe
laufen; da Refresh-Tokens rotierend/einmalig sind, gewann einer und
der andere bekam 401 -> Nutzer flog nach jedem Reload aus der Session.
Fix: AbortController im Session-Restore-Effect.
- Manifest verwies auf nicht existente PNG-Icons (404); durch
public/icon.svg ersetzt.
7 Vitest-Tests (Login-Formular, Users-Liste, JWT-Decode), Lint,
Typecheck und Build gruen. Kompletter Flow (Login, Reload-Persistenz,
Benutzer anlegen/deaktivieren, Abmelden, Redirect ohne Session) live
im Browser gegen den echten Server verifiziert.
ARCHITECTURE.md §3 legt fest: Access-Token im Memory/Context, Refresh-Token
als httpOnly-Cookie. Vorherige Implementierung gab beide Tokens im JSON-Body
zurueck - vor dem Frontend-Aufbau (das genau gegen diesen Vertrag baut)
korrigiert.
/api/v1/auth/login und /refresh setzen jetzt ein httpOnly-Cookie
(Path /api/v1/auth, SameSite=Lax, secure in production) und liefern nur
noch { accessToken } im Body. /refresh und /logout lesen den Refresh-Token
aus dem Cookie statt aus dem Request-Body.
e2e-Tests und Live-Smoke-Test (curl mit Cookie-Jar) verifizieren
Login/Rotation/Logout-Invalidierung. 12 e2e- + 26 Unit-Tests, Lint,
Typecheck und Build gruen.
AuthModule: Login/Refresh/Logout mit JWT-Access-Token + rotierendem,
gehashtem Refresh-Token (RefreshToken-Tabelle). JwtAuthGuard + RolesGuard
global via APP_GUARD registriert (gilt fuer jeden kuenftigen Endpunkt),
@Public()-Decorator als Ausnahme (Auth-Endpunkte, /health).
UsersModule: CRUD + Deactivate, nur ADMIN-Rolle, Passwoerter mit bcrypt
gehasht, nie im Response enthalten.
AuditModule: AuditLogService (Grundstruktur), wird von UsersService bei
create/update/deactivate injiziert und geschrieben.
Geteilte App-Konfiguration (Prefix/Versioning/Pipes/Helmet) aus main.ts
in src/app.config.ts extrahiert; e2e-Tests nutzten das vorher nicht und
bekamen dadurch 404 auf jede /api/v1/... Route - jetzt ueber
test/utils/create-test-app.ts geteilt, betrifft alle kuenftigen
e2e-Tests gleichermassen.
prisma/seed.ts legt einen Admin-User fuer lokale Entwicklung an.
26 Unit-Tests + 11 e2e-Tests (gegen echte Postgres-Testdatenbank) gruen,
Lint/Typecheck/Build gruen, Login/Refresh/Logout/Rollenpruefung manuell
gegen laufenden Server verifiziert.
2026-08-21 08:22:16 +02:00
936 changed files with 95552 additions and 547 deletions
@@ -26,6 +26,7 @@ Ein NestJS-Modul pro fachlichem Bereich, Reihenfolge entspricht Baureihenfolge i
| `DashboardModule` | Aggregations-Endpunkte | alle o.g. |
| `DashboardModule` | Aggregations-Endpunkte | alle o.g. |
| `SearchModule` | globale Suche, Filter-Bausteine | alle o.g. |
| `SearchModule` | globale Suche, Filter-Bausteine | alle o.g. |
| `ExportModule` | CSV/JSON/PDF-Export | alle o.g. |
| `ExportModule` | CSV/JSON/PDF-Export | alle o.g. |
| `ErpExportModule` | Nur lesender Inventar-Spiegel für ERPNext (`/api/v1/export/inventar/...`, Recht `erp-inventory:read`), Vertrag in [`docs/erp-inventar-export.md`](docs/erp-inventar-export.md) | Products, Bottling, ProductionBatches, Stock, Harvests |
Jedes Modul: `*.controller.ts`, `*.service.ts`, `dto/`, `*.module.ts`, `*.spec.ts` (Unit), `test/*.e2e-spec.ts` (Integration). Rollenprüfung über `@Roles()`-Decorator + `RolesGuard`, angewendet pro Endpunkt – niemals nur UI-seitig.
Jedes Modul: `*.controller.ts`, `*.service.ts`, `dto/`, `*.module.ts`, `*.spec.ts` (Unit), `test/*.e2e-spec.ts` (Integration). Rollenprüfung über `@Roles()`-Decorator + `RolesGuard`, angewendet pro Endpunkt – niemals nur UI-seitig.
@@ -59,11 +60,11 @@ Datenzugriff ausschließlich über TanStack Query gegen die Backend-API (kein di
## 3. Auth-Flow
## 3. Auth-Flow
1. Login (`POST /api/v1/auth/login`) mit E-Mail/Passwort → Access-Token (kurzlebig, ~15 min, JWT) + Refresh-Token (langlebig, ~7 Tage, in DB als Hash gespeichert, rotierend).
1. Login (`POST /api/v1/auth/login`) mit E-Mail/Passwort → Access-Token (15 min, JWT HS256, `iss=apicore`, `aud=apicore:access`) + Refresh-Token (in DB als Hash, rotierend). Bei aktiver MFA zuerst ein Zwischen-Token (`aud=apicore:mfa-challenge`), das nie als Access-Token gilt.
2. Frontend hält Access-Token im Memory/React-Context, Refresh-Token als httpOnly-Cookie.
2. Frontend hält Access-Token im Memory/React-Context, Refresh-Token als httpOnly-Cookie (`Secure`, `SameSite=Lax`, Pfad `/api/v1/auth`).
3. Bei 401 automatischer Refresh über `POST /api/v1/auth/refresh`, danach Retry.
3. Bei 401 automatischer Refresh über `POST /api/v1/auth/refresh`, danach Retry. Rotation atomar; Wiederverwendung eines alten Tokens (nach 10 s Gnadenfrist für parallele Tabs) beendet alle Sitzungen. Spätestens `SESSION_MAX_AGE` (7 Tage) nach dem Login ist neu anzumelden.
4.Jeder geschützte Endpunkt: `JwtAuthGuard` (Authentifizierung) +`RolesGuard` (Autorisierung nach Rolle aus Abschnitt 3).
4.Guards in dieser Reihenfolge: `ThrottlerGuard` → `JwtAuthGuard` (Authentifizierung, Gültigkeit frisch aus der DB je Anfrage) →`PermissionsGuard` (Berechtigung, fail-closed).
5.API-Client-Rolle nutzt dieselben JWT-Endpunkte mit client-credentials-artigem Service-Account (kein OAuth2-Server im MVP, siehe Annahmen).
5.Maschinen-Zugriff (Wabenhain) nur per API-Schlüssel mit Scope-Allowlist und optionaler IP-Allowlist, siehe Annahme 14 und Abschnitt 9.
## 4. Datenfluss / Rückverfolgbarkeit
## 4. Datenfluss / Rückverfolgbarkeit
@@ -95,7 +96,62 @@ Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige
4.**API-Client-Rolle im MVP**: kein vollwertiger OAuth2/Client-Credentials-Server. Ein API-Client ist ein `User` mit Rolle `API_CLIENT` und einem langlebigen Service-Refresh-Token; vollwertiges Scopes-/Client-Management ist SOLL/KANN.
4.**API-Client-Rolle im MVP**: kein vollwertiger OAuth2/Client-Credentials-Server. Ein API-Client ist ein `User` mit Rolle `API_CLIENT` und einem langlebigen Service-Refresh-Token; vollwertiges Scopes-/Client-Management ist SOLL/KANN.
5.**Backup-fähige Struktur**: kein eigenes Backup-Tooling im Code. Umgesetzt als: reproduzierbare Prisma-Migrationen (kein manuelles Schema-Drift), dokumentierte `pg_dump`/PITR-Strategie in diesem Dokument (Betrieb obliegt dem bestehenden Cluster/DBA), `/health`-Endpoint für Betriebsüberwachung.
5.**Backup-fähige Struktur**: kein eigenes Backup-Tooling im Code. Umgesetzt als: reproduzierbare Prisma-Migrationen (kein manuelles Schema-Drift), dokumentierte `pg_dump`/PITR-Strategie in diesem Dokument (Betrieb obliegt dem bestehenden Cluster/DBA), `/health`-Endpoint für Betriebsüberwachung.
6.**Deviations vom Leitentscheidungs-Rahmen**: bisher keine. Wird ergänzt, falls während der Umsetzung ein konkreter Grund für eine Abweichung von NestJS/Prisma/Next.js/Tailwind/shadcn/TanStack Query auftritt.
6.**Deviations vom Leitentscheidungs-Rahmen**: bisher keine. Wird ergänzt, falls während der Umsetzung ein konkreter Grund für eine Abweichung von NestJS/Prisma/Next.js/Tailwind/shadcn/TanStack Query auftritt.
7.**Dokumentenspeicher als lokales Volume (Modul 29)**: `DocumentsModule` speichert Uploads unterhalb von `DOCUMENTS_STORAGE_PATH` auf dem lokalen Dateisystem des Backend-Pods, keine Objektspeicher-Anbindung (S3-kompatibel o.ä.) im MVP/v1.x. Der Pfad ist über die ConfigMap austauschbar, wie in Abschnitt 27 des Lastenhefts vorgesehen. Wichtig für den Betrieb: das aktuelle `deploy/k8s`-Deployment läuft mit `replicas: 2` ohne geteiltes Volume – für konsistente Uploads über beide Pods hinweg muss vor Produktivbetrieb mit Dokumenten-Feature ein `ReadWriteMany`-PVC (z.B. NFS- oder EFS-basiert) gemountet und `DOCUMENTS_STORAGE_PATH` darauf gesetzt werden, sonst sieht nur der Pod, der die Datei entgegengenommen hat, sie auch beim Download wieder.
8.**Entra ID SSO ohne Auto-Provisioning**: `POST/GET /api/v1/auth/sso/*` implementiert Authorization Code + PKCE gegen Entra ID über `openid-client`. Der E-Mail-Claim aus dem ID-Token wird ausschließlich gegen einen bereits existierenden, aktiven `User`-Datensatz gematcht – es wird kein neuer Benutzer automatisch angelegt (Just-in-Time-Provisioning), passend zum bestehenden Modell, dass Admins Benutzer manuell anlegen. Eine erfolgreiche SSO-Anmeldung überspringt die App-eigene TOTP-MFA-Abfrage, da der Identity Provider bereits eine eigene (typischerweise stärkere) Authentifizierung durchgesetzt hat. Der PKCE-`code_verifier` wird kurzlebig (5 min) in einem httpOnly-`SameSite=Lax`-Cookie zwischengespeichert statt serverseitig, damit der Flow ohne Sticky-Sessions über mehrere Backend-Replicas hinweg funktioniert; der Rückweg vom Callback zum Frontend nutzt ein kurzlebiges (60 s), zweckgebundenes JWT statt des echten Access-Tokens in der Redirect-URL. `OIDC_ISSUER_URL`/`OIDC_CLIENT_ID`/`OIDC_CLIENT_SECRET`/`OIDC_REDIRECT_URI` kommen ausschließlich aus Environment-Variablen; fehlen sie, meldet `GET /api/v1/auth/sso/status``enabled: false` und die Login-Seite blendet den "Mit Microsoft anmelden"-Button aus, statt beim Start zu scheitern.
9.**M365-Kalender-Synchronisation als einseitiger, asynchroner Push**: Abschnitt 4.25 verlangt nur einen internen Betriebskalender, keine externe Anbindung – diese Funktion ist eine reine Erweiterung ohne Lastenheft-Vorgabe. Umgesetzt als `CalendarSyncModule` (`ApiCore-Backend/src/calendar-sync/`): App-only Graph-API-Zugriff per Client-Credentials-Flow (`GraphClientService`) statt delegierter Anmeldung, da die Synchronisation unbeaufsichtigt im Hintergrund laufen muss und nicht an eine interaktive Benutzersitzung gebunden sein darf. Nutzt bewusst dieselbe Entra-App-Registrierung wie das SSO-Login (`OIDC_ISSUER_URL`/`OIDC_CLIENT_ID`/`OIDC_CLIENT_SECRET`, ausschließlich Env-Variablen) statt einer zweiten, separaten App-Registrierung – ein Enterprise-App-Objekt im Tenant genügt, sofern es zusätzlich zu den delegierten Login-Scopes auch die Application-Permission „Calendars.ReadWrite“ (Admin-Consent) erhält; die Tenant-ID wird dafür aus dem Pfad von `OIDC_ISSUER_URL` abgeleitet, statt eine weitere Konfigurationsvariable zu benötigen. Die Übertragung ist bewusst nur einseitig (ApiCore → M365-Gruppenkalender, adressiert über `groupId`); Änderungen, die direkt im M365-Kalender vorgenommen werden, fließen nicht zurück. Statt eines synchronen Push im Request-Pfad (wie beim Webhooks-Modul) nutzt `CalendarSyncOutboxEntry` ein Outbox-Muster mit minütlichem `@Cron`-Worker: Graph-API-Aufrufe sind zu langsam/fehleranfällig (Token-Refresh, Drosselung, transiente 5xx), um eine Durchsicht-/Aufgaben-Erstellung darauf warten zu lassen; fehlgeschlagene Versuche werden bis zu 5-mal wiederholt, danach bleibt der Eintrag `FAILED` bis zu einem manuellen "Jetzt synchronisieren" oder einer erneuten Änderung des Quelldatensatzes. Die Outbox ist sicher bei mehreren Replicas: Jeder Eintrag wird vor der Arbeit per bedingtem Update (`claimedAt`) reserviert, ein Claim älter als 5 min gilt als verwaist und wird übernommen, Graph-Requests haben ein 30-s-Timeout. `revision` sorgt dafür, dass eine Änderung während des Syncs nicht verloren geht (Eintrag bleibt PENDING, dasselbe Event wird beim nächsten Lauf aktualisiert); ein Abbruch während des Syncs löscht das gerade geschriebene Event. Ein neues Graph-Event entsteht beim Aktualisieren nur, wenn das alte nicht mehr existiert (404/410); andere Fehler gelten als fehlgeschlagener Versuch. Jedes Anlegen schickt eine `transactionId` (aus Eintrag, Revision und ersetztem Event), damit Graph eine Wiederholung nach einem Timeout nicht doppelt anlegt; „Jetzt synchronisieren“ startet den Lauf im Hintergrund (202, `running` im Status) und setzt FAILED-Einträge auf PENDING zurück. `groupId` muss eine Entra-Objekt-ID (GUID) sein; `""` oder `null` löscht sie; sie wird in Graph-URLs kodiert. Fehler aus Prisma werden nur mit Name, Code und Modell geloggt (`src/common/loggable-error.ts`), nie mit Abfragewerten. **Offene Entscheidung (2026-10-12):** Laut Microsoft-Doku (`group-post-events`) unterstützt `POST /groups/{id}/events` keine Application-Permission, nur delegiertes `Group.ReadWrite.All`. Mit dem App-only-Token oben kann das Anlegen im Gruppenkalender deshalb in Produktion scheitern (nicht gegen einen echten Tenant geprüft). Auswege: Zielkalender auf ein Ressourcen- oder Benutzerpostfach umstellen (`/users/{upn}/calendar/events`, App-only erlaubt, per Application Access Policy begrenzt) oder delegierter Zugriff über ein Dienstkonto. Pro Quelltyp (Aufgabe/Durchsicht/Behandlung/Fütterung/Ernte/Königinnen-Zuordnung) ist die Synchronisation einzeln über die GUI (Verwaltung > Automatisierung > Kalender-Sync) abschaltbar; Kalendereinträge werden als ganztägige Graph-Events (`isAllDay: true`) angelegt, um Zeitzonen-/Uhrzeit-Mehrdeutigkeiten zu vermeiden, da die zugrundeliegenden Fachdaten selbst keine Uhrzeit führen.
10.**Wartezeit manuell statt aus Wirkstoff-Datenbank abgeleitet**: `Treatment.withdrawalPeriodDays` wird pro Behandlung von Hand eingetragen (laut Packungsbeilage des verwendeten Mittels), statt automatisch aus Produktname/Wirkstoff hergeleitet zu werden. Eine hartkodierte Zuordnung Produkt→Wartezeit wäre eine faktische, rechtlich relevante Behauptung über Tierarzneimittel-Zulassungen, die im Code weder aktuell gehalten noch verlässlich verifiziert werden kann; die Verantwortung für den korrekten Wert bleibt beim Imker. Die App warnt in der Ernte-Erfassung nur, wenn ein Wert hinterlegt wurde und dessen Fenster (`endDate ?? date` + `withdrawalPeriodDays`) noch nicht abgelaufen ist – bezogen auf das Erntedatum (`GET /treatments/withdrawal-status?colonyIds=…&at=JJJJ-MM-TT`, ohne `at` gilt jetzt; Behandlungen, die erst nach diesem Datum beginnen, zählen nicht) – sie blockiert die Ernte nicht hart, da Ernte-Datensätze auch nachträglich für bereits erfolgte, reale Ernten erfasst werden.
11.**Idempotenz per Opt-in**: Anlege-Endpunkte, die Formulare und die Offline-Warteschlange wiederholen können (Durchsicht, Behandlung, Fütterung, Varroa, Schwarmkontrolle, Ernte, Aufgabe, Abfüllung, Lagerbewegung), tragen `@Idempotent()`. Der Client sendet je Vorgang einen `Idempotency-Key`; eine Wiederholung liefert die gespeicherte erste Antwort (`IdempotencyRecord`, 90 Tage). Bewusst kein globaler Interceptor, da gespeicherte Antworten sonst Geheimnisse (MFA-Setup, Tokens) enthalten könnten. Die Tabelle ist flüchtig und nicht Teil des Backups. Idempotenz schützt nur, wenn nach dem Commit nichts mehr fehlschlägt: Darum laufen alle Seiteneffekte nach dem Commit (`AutomationService.evaluate*`, `CalendarSyncService.enqueue`/`cancel`, `WebhooksService.dispatch`) best-effort über `runAfterCommit` (`src/common/after-commit.ts`) – sie loggen auf Error-Level und werfen nie. Was das Schreiben verhindern muss (z. B. `generateNumber`, Validierung), wirft weiter. Doppelte Automatik-Aufgaben verhindert eine Zeilensperre auf der Regel (`SELECT … FOR UPDATE`). Dauerhaft zugestellte Seiteneffekte bringt erst das Ereignis-Rückgrat (Spec `docs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md`).
12.**Automatische Aufgaben sind Vorschläge**: `Task.sourceAutomationTrigger` kennzeichnet von Regeln erzeugte Aufgaben. Je Regel und Objekt existiert höchstens eine offene Aufgabe. Keine voreingestellte Frist (fachlich nicht belastbar ableitbar); optional `config.dueInDays` je Regel. Die Varroa-Schwelle (Milben/Tag) wird nur mit Tagesraten des natürlichen Milbenfalls verglichen. Die Tagesrate wird bei jeder Korrektur einer Eingangsgröße (Methode, Anzahl, Dauer) neu berechnet; ein mitgesendeter, vom gespeicherten abweichender Wert gilt als manuell gesetzt und gewinnt.
14.**API-Schlüssel für Maschinen-Zugriffe**: Integrationen (Wabenhain-Shop) nutzen einen Schlüssel `ack_…` im `Authorization: Bearer`-Header statt eines Service-Kontos mit Passwort. Gespeichert wird nur der SHA-256-Hash (32 Zufallsbytes, daher kein langsamer KDF nötig); der Klartext erscheint einmal bei der Erstellung. Ein Schlüssel ist kein Benutzer: `JwtAuthGuard` setzt `request.apiKey` statt `request.user`, `PermissionsGuard` lässt ihn nur auf Routen, deren Berechtigung in seinen Scopes steht –`@AnyAuthenticatedUser`-Routen nie. Erlaubte Scopes sind eine kurze Allowlist (`api-keys/api-key-scopes.ts`: `shop-sync:read`, `shop-sync:write`, `erp-inventory:read`); je angebundenem System ein eigener Schlüssel. Verwaltung mit `api-keys:manage`. Schlüssel sind bewusst nicht Teil des Backups: nach einer Wiederherstellung wird ein neuer Schlüssel ausgestellt.
13.**Lebende vs. beendete Völker**: Nur `DISSOLVED`, `SOLD`, `DECEASED`, `MERGED` sperren Umsetzen/Teilen/Vereinigen/Königinzuweisung (`colonies/colony-status.ts`). `WEAK`, `CONSPICUOUS`, `QUEENLESS` bleiben bearbeitbar.
15.**Bestandsbuch aus Behandlungen, nicht als eigenes Register**: Das Bestandsbuch nach Art. 108 VO (EU) 2019/6 wird aus `Treatment` erzeugt (`export/treatment-log.ts`); Lieferant, Erwerbsbeleg und verschreibender Tierarzt sind Freitextfelder der Behandlung, Name/Anschrift des Halters liegen in `SiteSettings`. Der Stand am Behandlungstag wird beim Anlegen als `Treatment.apiaryId` festgehalten (Umsetzen ändert ihn nicht nachträglich); für Altbestand wird er aus `ColonyApiaryHistory` abgeleitet und im PDF bei Unsicherheit markiert. Fehlende Pflichtangaben werden als „FEHLT“ gedruckt statt still weggelassen. Keine Wartezeit-Automatik (siehe 10).
16.**Überwinterung als eigene Einträge statt Volksstatus**: `WinterCheck` (eindeutig je Volk, `winterYear`, Saison) hält Ein- und Auswinterung; ab Juli gehört ein Datum zum Winter desselben Jahres. Verlustursachen sind bewusst nur die COLOSS-Kategorien, keine Diagnosen. Die Übersicht gruppiert nach dem aktuellen Stand des Volkes. Ein Verlust ändert den Volksstatus nur auf ausdrücklichen Wunsch (`markColonyDeceased`). Neue Rechte vergibt die Migration an Nicht-System-Rollen, weil der Katalog-Sync nur System-Rollen berechtigt.
17.**Schutz gegen Rechteausweitung in der Benutzerverwaltung**: Wer Benutzer pflegt, darf nur Konten verwalten (Passwort/MFA zurücksetzen, E-Mail ändern, (de)aktivieren) und Rollen vergeben, deren Rechte er selbst vollständig hat (`UsersService.assertWithinCallerPermissions`), analog zur Rollenbearbeitung.
18.**SSO-Bindung an die IdP-Identität**: Die erste Microsoft-Anmeldung bindet das Konto an `"<iss>|<oid>"` (`User.ssoSubject`); danach zählt nur noch diese Identität, nicht der unverifizierte E-Mail-Claim. Mandantenübergreifende Issuer (`common`/`organizations`/`consumers`) schalten SSO ab. E-Mails werden ohne Groß-/Kleinschreibung verglichen.
19.**Backups versiegelt**: AES-256-GCM über das gzip-Bundle, Schlüssel `BACKUP_ENCRYPTION_KEY` (sonst per HKDF aus `JWT_ACCESS_SECRET`). Nur versiegelte Dateien stellen vollständig wieder her (und überschreiben). Eine unversiegelte (alte v2-)Datei darf nur die 27 Stamm- und Feldtabellen der Erlaubt-Liste `LEGACY_UNSEALED_TABLES` ergänzen – fehlende Zeilen anlegen, bestehende nie ändern. Alles andere (Benutzer, Rollen, Webhooks, Audit-Log, Kalender-Sync, Backup- und Website-Einstellungen, Nummernkreise, Bestands- und Chargenbewegungen, Shop-Anfragen, Rohstoffe/Herstellung, Dokumente, Benachrichtigungen und jede künftige Tabelle) kommt nur aus versiegelten Dateien; die Oberfläche listet übersprungene Tabellen auf; Import verlangt Passwort (+ TOTP). Webhook-Secrets liegen verschlüsselt in der DB (Schlüssel aus `JWT_ACCESS_SECRET`) – für eine Wiederherstellung in einem anderen Deployment mit anderem `JWT_ACCESS_SECRET` müssen Webhook-Secrets und MFA neu eingerichtet werden.
20.**Bearbeiten-Regel für `null`** (Grundsatz „alles bearbeitbar“): In jeder Bearbeiten-Anfrage bleibt ein weggelassenes Feld unverändert, `null` leert ein Feld, das leer sein darf, und `null` auf einem Pflichtfeld wird mit 400 abgelehnt (statt still ignoriert oder als 500). Umgesetzt über `PartialType(..., { skipNullProperties: false })` in den Update-DTOs, `@IsOptionalNotNull()` (`src/common/validation/is-optional-not-null.ts`) für weglassbare, aber nicht leerbare Felder und `optionalDate()` (`src/common/patch-values.ts`) für leerbare Datumsfelder; `update-dto-null-handling.spec.ts` hält den Vertrag je DTO fest. Das Frontend sendet geleerte optionale Felder über `emptyToNull` (`ApiCore-Frontend/src/lib/forms/empty-to-null.ts`) als `null`.
21.**Lebende Völker**: „Lebend“ heißt jeder Status außer `DISSOLVED`, `SOLD`, `DECEASED`, `MERGED`– also auch `WEAK`, `CONSPICUOUS`, `QUEENLESS`. Backend-Abfragen filtern über `LIVING_COLONY_WHERE` (`colonies/colony-status.ts`), das Frontend über `isLivingColony` (`lib/colonies/status.ts`); `status = ACTIVE` steht nur dort, wo wirklich genau dieser Status gemeint ist.
## 7. Kubernetes
## 7. Kubernetes
`deploy/k8s/` enthält je Projekt: `deployment.yaml`, `service.yaml`, `ingress.yaml`, sowie `configmap.example.yaml` und `secret.example.yaml` (Platzhalter, keine echten Werte). Backend exponiert `GET /health` (liveness) und `GET /health/ready` (readiness, prüft DB-Verbindung). Beide Projekte je ein schlankes Multi-Stage-Dockerfile (Build-Stage mit vollem `node_modules`, Runtime-Stage mit `npm ci --omit=dev` bzw. Next.js `output: 'standalone'`).
`deploy/k8s/` enthält je Projekt: `deployment.yaml`, `service.yaml`, `ingress.yaml`, sowie `configmap.example.yaml` und `secret.example.yaml` (Platzhalter, keine echten Werte). Backend exponiert `GET /health` (liveness) und `GET /health/ready` (readiness, prüft DB-Verbindung). Beide Projekte je ein schlankes Multi-Stage-Dockerfile (Build-Stage mit vollem `node_modules`, Runtime-Stage mit `npm ci --omit=dev` bzw. Next.js `output: 'standalone'`).
`deploy/k8s/postgres/` enthält das PostgreSQL-`StatefulSet` (ein Replica, `volumeClaimTemplates` für persistente Daten, `postgres:18-alpine`), den zugehörigen headless `Service` (`apicore-postgres`, DNS-Name fürs Backend) sowie `secret.example.yaml` für `POSTGRES_USER`/`POSTGRES_PASSWORD`/`POSTGRES_DB`. `deploy/k8s/backend/secret.example.yaml`s `DATABASE_URL` zeigt auf diesen Service.
## 8. Backup & Restore
**Schema-Migrationen (Reproduzierbarkeit):** Ausschließlich `prisma/migrations/` als Quelle der Wahrheit. Lokal/Entwicklung: `npx prisma migrate dev`. Gegen jede Produktions- oder Staging-Datenbank ausschließlich `npx prisma migrate deploy` (idempotent, wendet nur fehlende Migrationen an, kein Schema-Drift). `prisma db push` wird an keiner Stelle im Code, in Skripten oder im Dockerfile verwendet und ist für Produktions-/Staging-Umgebungen untersagt. Der Backend-Container führt `migrate deploy` und danach `npm run seed` selbst beim Start aus, bevor der Server Verbindungen annimmt (Dockerfile-`CMD`: `npm run migrate:deploy && npm run seed && node dist/main.js`) – kein separater Kubernetes-`Job`. Bei `replicas: 2` starten mehrere Pods potenziell gleichzeitig; das ist unproblematisch, da Prisma für `migrate deploy` einen datenbankseitigen Advisory-Lock nutzt und `prisma/seed.ts` per Unique-Constraint auf `User.email` idempotent ist (verlierender Pod bekommt einen `P2002`-Fehler, den `seed.ts` abfängt und als No-op behandelt statt den Container abstürzen zu lassen) – eine Instanz wendet die Migrationen/den Seed an, die anderen finden danach nichts mehr zu tun. `deploy/k8s/backend/deployment.yaml` hat dafür eine `startupProbe` mit großzügigem `failureThreshold`, damit liveness/readiness den Container nicht neu starten, während die (beim allerersten Deploy ggf. länger laufende) Migration noch durchläuft.
**Erster Admin-Zugang:**`prisma/seed.ts` legt beim allerersten Start (leere `User`-Tabelle) einen Administrator an (`SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD`, Default `[email protected]`/`ChangeMe123!`) und gibt URL (`FRONTEND_ORIGIN`), Benutzername und Passwort einmalig im Container-Log aus. Existiert der User schon (auch deaktiviert), passiert nichts – kein erneutes Log, kein erneutes Anlegen, damit spätere Passwortänderungen nicht durch veraltete Logs verwirrend überschrieben wirken und Zugangsdaten nicht bei jedem Neustart in die Logs wandern. Ersetzen: neuen Admin über `/settings/users` anlegen, dann den alten deaktivieren (Löschen gibt es nicht, siehe "Historie statt Löschen") – die Lockout-Sperre in `UsersService.assertNotLastSystemRoleUser` verhindert ohnehin, den letzten aktiven Admin zu deaktivieren.
**Datensicherung:** Seit der Entscheidung, PostgreSQL selbst im Cluster zu betreiben (siehe CLAUDE.md), liegt die Backup-Verantwortung bei diesem Projekt, nicht mehr bei einem externen Cluster-/DBA-Betrieb. Umgesetzt als anwendungsseitiges `BackupModule` (`ApiCore-Backend/src/backup/`) statt der ursprünglich hier vorgesehenen reinen `pg_dump`/PITR-Strategie:
- **Export/Import:** `BackupService.exportAll()`/`.importAll()` sichert alle fachlichen und Konfigurationsmodelle (Betriebsdaten inkl. Herstellung, Rohstoffe, Bewegungsbuch und Shop-Anfragen, Nummernkreise, Benutzer/Rollen/Rechte, Audit-Log, Website-/Backup-/Kalender-Einstellungen samt Kalender-Outbox, hochgeladene Dokumente inkl. Dateiinhalt als Base64) als eine versionierte JSON-Datei (Bundle-Version 3; ältere Bundles bleiben einspielbar). Bewusst ausgenommen sind nur Permission (Katalog aus dem Code), RefreshToken, ConsumedToken, ApiKey (Annahme 14) und IdempotencyRecord (Annahme 11); die Liste mit Begründung steht in `MODELS_EXCLUDED_FROM_BACKUP`, ein Test erzwingt sie für jedes neue Prisma-Modell. Import ist upsert-basiert (Rollen/Berechtigungen über `key`/`resource+action` statt roher `id`, damit ein Restore auch in ein anderes Deployment funktioniert) und läuft in einer einzigen `$transaction`.
- **Automatischer Zeitplan:** `BackupSchedulerService` registriert einen `@nestjs/schedule`-Cron-Job, der die Bundle-Datei periodisch auf `BACKUP_STORAGE_PATH` schreibt und alte Stände nach `BACKUP_RETENTION_COUNT` rotiert. Pfad/Zeitplan/Aufbewahrung sind sowohl per Env-Variable als auch über die GUI (Verwaltung > Backup) konfigurierbar – eine gesetzte Env-Variable hat dabei immer Vorrang vor dem GUI-Wert. Bei `replicas: 2` verhindert ein Datei-Mtime-Guard (kein DB-Lock nötig), dass beide Pods im selben Tick doppelt sichern; ohne gesetzten Pfad bleibt der Zeitplan inaktiv, manueller Export/Import über die UI funktioniert davon unabhängig immer.
- **Restore:** Über `POST /backup/import` (bzw. den Import-Dialog unter Verwaltung > Backup) eine zuvor exportierte Bundle-Datei einspielen. Import-Limit `BACKUP_MAX_IMPORT_BYTES` (Standard 50 MB, höchstens 256 MB, weil der Upload vollständig im RAM des 512-Mi-Pods verarbeitet wird); Export und Zeitplan prüfen jede Sicherung dagegen und warnen in Log und UI (Verwaltung > Backup), bevor eine Datei entsteht, die sich im Notfall nicht mehr einspielen ließe.
Ein rohes `pg_dump`/PITR-Backup auf Infrastrukturebene bleibt als zusätzliche, komplementäre Absicherung für den Katastrophenfall (z. B. korrupte Datenbankdateien, nicht nur fachliche Daten) sinnvoll, ist aber nicht mehr der primäre, hier dokumentierte Mechanismus – die in-App-Lösung deckt den fachlichen Anwendungsfall "Daten sichern/übertragen" bereits vollständig ab.
## 9. API-Sicherheit (Stand Oktober 2026)
Grundlage: OWASP API Security Top 10 (2023) und OWASP ASVS Level 2. Umsetzung und Begründung je Punkt in `docs/pruefung-2026-09.md`, Abschnitt „API-Sicherheit“.
| Bereich | Maßnahme |
|---|---|
| Authentifizierung | JWT mit festem Algorithmus, Issuer und Audience je Zweck; MFA (TOTP) mit Replay-Schutz; Kontosperre 15 min nach 5 Fehlversuchen; gleiche Antwort für unbekannte/gesperrte Konten; bcrypt auf jedem Pfad |
| Passwörter | mind. 12 Zeichen, max. 72 Byte, Abgleich mit Have I Been Pwned (k-Anonymität, abschaltbar per `PASSWORD_BREACH_CHECK=false`) |
| Sitzungen | Refresh-Rotation atomar, Diebstahlerkennung, max. 7 Tage; Einmal-Tokens für SSO-Abschluss und MFA-Schritt |
| Autorisierung | fail-closed `PermissionsGuard`; keine Rechteausweitung über Benutzer-/Rollenpflege; Suche nur über lesbare Objektarten |
| Maschinen-Zugriff | API-Schlüssel nur als Hash (Suche über den Hash, kein Vergleich von Geheimnissen), Scope-Allowlist (`shop-sync:read`, `shop-sync:write`, `erp-inventory:read`), IP-Allowlist, widerrufbar, befristbar; Ersteller kann nur eigene Rechte vergeben; nach 20 ungültigen Schlüsseln in 15 min je IP (IPv6: je /64) `429` für weitere ungültige – gültige Schlüssel nie, falsch aufgebaute Tokens zählen nicht; Warnung beim Start, wenn `TRUST_PROXY` in Produktion fehlt; Logs nur mit Schlüssel-Präfix |
| Ausgehende Aufrufe | Webhooks: Ziel-IP beim Verbindungsaufbau geprüft (kein DNS-Rebinding), nur Port 80/443, https in Produktion, 5 s Timeout, Signatur über Zeitstempel+ID+Body |
| Transport/Antworten | `Cache-Control: no-store` (außer öffentliche Chargen-Seite), CSP `default-src 'none'`, `frame-ancestors 'none'`, keine ETags; Produktion startet nur mit https-`FRONTEND_ORIGIN` und sicheren Cookies |
| Rate-Limits | Throttler vor der Authentifizierung (100/min, Login/MFA/Refresh/Logout 10/min, öffentliche Chargen 30/min); Client-IP über `TRUST_PROXY` |
| Daten | Backups verschlüsselt und signiert, Dateien 0600; CSV-Export ohne Formel-Injection; Logs ohne Query-Strings |
**Bekannte Restpunkte (bewusst nicht umgesetzt):**
- Rate-Limits zählen je Pod im Speicher; bei 2 Replikas verdoppelt sich das Limit. Brute-Force-Schutz hängt nicht daran, sondern an der Kontosperre in der DB. Ein gemeinsamer Speicher (Redis) wäre der nächste Schritt, falls Lastspitzen ein Thema werden.
- Eine E-Mail-Änderung im eigenen Profil meldet „bereits vergeben“, wenn die Adresse existiert. Das erfordert eine Anmeldung plus aktuelles Passwort; eine generische Meldung würde Nutzer nur verwirren.
// Behind Cloudflare and Traefik one hop is wrong: req.ip becomes the
// proxy, so rate limits, API-key failure limits and IP allowlists act
// on a shared address instead of the client.
bootstrapLogger.warn(
'TRUST_PROXY is not set in production - client IPs for rate limits and API-key allowlists are likely the proxy address; set it as in deploy/k8s/backend/configmap.example.yaml',
);
}
// No ETags: they let caches and proxies revalidate (and keep) personal
// responses that are meant to be no-store.
app.set('etag',false);
// Default 100kb JSON body limit is too small for the avatar upload, which
// sends the image as a base64 data: URL in the JSON body (not multipart -
// see profile/dto/upload-avatar.dto.ts for why) - 5mb comfortably covers
// the 2MB decoded size cap plus base64 overhead.
app.useBodyParser('json',{limit:'5mb'});
app.use(
helmet({
// This server only answers JSON and files. A strict policy means even
// a response that is somehow rendered as a page can load nothing and
// cannot be framed. The Swagger UI (opt-in) needs scripts, so it keeps
Loaded 100 of 936 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.