Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks:
- run roles api/worker/job/all with shared startup checks (Task 1)
- schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2)
- audit, notifications and domain events written in the same transaction (Task 3)
- dispatcher with SKIP LOCKED claims, retries and stale release (Task 4)
- worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5)
- worker process and entry points (Task 6)
- throttler counters in Postgres (Task 19)
- separate keys per purpose with key ring (Task 20)
- public origin page without exact location (Task 22)
- shop keys split into read and write (Task 23)
- Kustomize base, components, overlay encry, CI validation (Task 26)
Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks:
- brood frames, read endpoints, apiary overview and offline pack
- network basics, offline queue v3, navigation sheets, form building blocks
- new inspection with last-time hints and weather, varroa per day,
treatment prefill, apiary page with round
- inspection photo (Task 10) is implemented, review pending
Not rolled out. Remaining tasks follow in later commits.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Backend
- Backup covers every Prisma model (bundle v3); a guard test fails for any
new model that is neither backed up nor explicitly excluded. Unsealed
bundles may only add rows to 27 legacy master/field tables and never
overwrite; counters, ledgers, settings, roles etc. need a sealed file.
Shared import limit BACKUP_MAX_IMPORT_BYTES (default 50 MB, max 256 MB)
with warnings in log and UI; replaced document files are removed after
commit.
- Post-commit side effects (automation, calendar sync, webhooks) run via
runAfterCommit and never turn a committed write into a 500, so retries
with the same Idempotency-Key no longer create duplicates.
- Calendar outbox is multi-replica safe: per-entry claims with stale
takeover, revision-based settle, new Graph event only on 404/410,
transactionId on create, 30 s Graph timeout, URL-encoded ids, GUID
validation for groupId, "run now" in background and resets FAILED.
- Automation tasks are deduplicated under a row lock on the rule.
- Update DTOs follow one null rule: omitted = unchanged, null clears a
nullable field, null on a required field is a 400.
- Varroa mites/day is recomputed on correction; withdrawal status is
checked against the harvest date (?at=), bounded colonyIds; inspection
create is audited; updates re-enqueue calendar entries.
- Dashboard, statistics and apiary/colony guards treat WEAK, CONSPICUOUS
and QUEENLESS colonies as living.
- Prisma errors and body-parser errors are logged without query values or
request bodies.
- Migration 20261012100000_calendar_sync_claims; schema index name aligned
with the existing database object.
Frontend
- Sticky save bar sticks again (no scroll container ancestors, card uses
overflow-clip), safe-area insets, scroll-padding, FAB hidden on forms,
sticky desktop sidebar.
- Inspections record yes / no / not checked instead of silent defaults.
- All living colonies are selectable; shared RecordSelect, EnumSelect,
ChoiceGroup and TriStateField replace per-page copies; selects show
names instead of ids; labels point at real controls.
- Emptied optional fields are sent as null (emptyToNull) so every field
can be cleared; dates default to the local day and are sent via
toApiDate.
- Harvest withdrawal warning uses the harvest date; backup page warns when
a backup outgrows the import limit and lists skipped tables.
Build and docs
- Docker builds fail unless lint, typecheck and unit tests pass.
- ARCHITECTURE.md updated (assumptions 9-12, 19-21, section 8) and spec
docs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md
for the backbone (api/worker/job roles, domain event outbox) and the
feature roadmap T3-T7.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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]>
- 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]>
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]>
- 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]>
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]>
- 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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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]>
- 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.
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]>
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]>
- 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)
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.
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).
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]>
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]>
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).
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.
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.
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.
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.
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.
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).
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.