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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>