Commit Graph
354 Commits
Author SHA1 Message Date
Nick SzaunigandClaude Opus 5.5 ed0a000534 feat(ui): remaining capture forms with sections, tap choices and drafts
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]>
2026-10-01 12:25:24 +02:00
Nick SzaunigandClaude Opus 5.5 49f3db5b56 feat(ui): storage, map, search, wintering and statistics without dead ends
- 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]>
2026-10-01 12:22:32 +02:00
Nick SzaunigandClaude Opus 5.5 0e46f666aa fix(ui): backup schedule shows a load error with retry, not "not configured"
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 12:17:56 +02:00
Nick SzaunigandClaude Opus 5.5 d57162e709 feat(ui): undo for deleted label elements, warn before leaving unsaved changes
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]>
2026-10-01 12:17:36 +02:00
Nick SzaunigandClaude Opus 5.5 b4c8d80c33 fix(ui): number keyboard for intervals in recurring task dialogs
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 12:16:19 +02:00
Nick SzaunigandClaude Opus 5.5 549a75988e feat(ui): webhooks as cards with events in words, calendar sync without env jargon
- 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]>
2026-10-01 12:15:54 +02:00
Nick SzaunigandClaude Opus 5.5 a21b1665e7 feat(ui): colony edit dialogs with sections and tap ratings, confirmed merge
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]>
2026-10-01 12:14:07 +02:00
Nick SzaunigandClaude Opus 5.5 9944aabbc3 feat(ui): list pages with retry on errors, a next step when empty, one main action
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]>
2026-10-01 12:14:07 +02:00
Nick SzaunigandClaude Opus 5.5 62e78498bf feat(ui): capture forms for treatments, feedings, colonies, apiaries, products, equipment
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]>
2026-10-01 12:14:07 +02:00
Nick SzaunigandClaude Opus 5.5 832997f885 feat(ui): users and audit log as readable cards with names and retry
- 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]>
2026-10-01 12:13:40 +02:00
Nick SzaunigandClaude Opus 5.5 bd2070436c feat(ui): calendar as a day list on phones, today named, every entry reachable
- 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]>
2026-10-01 07:38:09 +02:00
Nick SzaunigandClaude Opus 5.5 dff6c364c8 feat(ui): automation settings start with everyday tabs and confirm risky actions
- 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]>
2026-10-01 07:36:28 +02:00
Nick SzaunigandClaude Opus 5.5 78325af1fe feat(ui): touch-size controls, scrollable dialogs, motion levels everywhere, load states
- 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]>
2026-10-01 07:33:50 +02:00
Nick SzaunigandClaude Opus 5.5 32dfd8d8a7 feat(ui): calmer inspection form with sections, tap ratings and drafts
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]>
2026-10-01 07:32:20 +02:00
Nick SzaunigandClaude Opus 5.5 8b6f48b996 docs: API security model, audit results and operating notes
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]>
2026-10-01 07:27:22 +02:00
Nick SzaunigandClaude Opus 5.5 45efb08369 fix(auth): one-time SSO and MFA tokens, permission changes effective on every replica
- 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]>
2026-10-01 07:24:28 +02:00
Nick SzaunigandClaude Opus 5.5 fc6af8737b style(public): drop a needless escape in the lot number pattern
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 07:20:55 +02:00
Nick SzaunigandClaude Opus 5.5 0899576262 fix(api): tighter public lot endpoint, SSO callback from config, mapped 4xx errors
- 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]>
2026-10-01 07:20:33 +02:00
Nick SzaunigandClaude Opus 5.5 eafe925d60 fix(search): only search record kinds the caller may read
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]>
2026-10-01 07:18:33 +02:00
Nick SzaunigandClaude Opus 5.5 0ba8996e3a feat(auth): 12-character minimum, 72-byte maximum and breach check for new passwords
- 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]>
2026-10-01 02:47:09 +02:00
Nick SzaunigandClaude Opus 5.5 61e32474e4 fix(export): neutralize spreadsheet formulas in CSV exports
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]>
2026-10-01 02:43:44 +02:00
Nick SzaunigandClaude Opus 5.5 f1dc0de3e2 feat(api-keys): restrict keys to source IP addresses
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]>
2026-10-01 02:41:46 +02:00
Nick SzaunigandClaude Opus 5.5 9ae1d96a32 fix(api): throttle before auth, configurable proxy trust, no-store and a strict CSP
- 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]>
2026-10-01 02:37:31 +02:00
Nick SzaunigandClaude Opus 5.5 2de93bc087 fix(auth): atomic refresh rotation and a 7-day absolute session limit
- 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]>
2026-10-01 02:30:32 +02:00
Nick SzaunigandClaude Opus 5.5 5c666fe49a fix(auth): lock accounts after failed attempts, one-time TOTP codes, stronger MFA changes
- 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]>
2026-10-01 02:27:47 +02:00
Nick SzaunigandClaude Opus 5.5 79d20ee64e fix(webhooks): close DNS rebinding, time out deliveries, sign with timestamp
- 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]>
2026-10-01 02:17:40 +02:00
Nick SzaunigandClaude Opus 5.5 ebeb499a18 fix(backup): keep GUI backup paths under BACKUP_ROOT and limit the schedule
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]>
2026-10-01 02:14:18 +02:00
Nick SzaunigandClaude Opus 5.5 bd592adf66 fix(backup): seal backups and require step-up auth to restore
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]>
2026-10-01 02:12:43 +02:00
Nick SzaunigandClaude Opus 5.5 a25265b88a fix(sso): bind Microsoft sign-in to the IdP identity, not the email claim
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]>
2026-10-01 01:20:49 +02:00
Nick SzaunigandClaude Opus 5.5 c4a11b4704 fix(users): never manage or hand out more than the caller holds
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]>
2026-10-01 01:16:37 +02:00
Nick SzaunigandClaude Opus 5.5 e0bd7b2095 fix(auth): accept only access tokens as bearer credentials (MFA bypass)
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]>
2026-10-01 01:14:02 +02:00
Nick SzaunigandClaude Opus 5.5 fac66882fa feat: Ein-/Auswinterung und Winterverluste je Volk
- 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]>
2026-09-28 17:32:03 +02:00
Nick SzaunigandClaude Opus 5.5 0d23963f07 feat(export): Bestandsbuch für Tierarzneimittel nach Art. 108 VO (EU) 2019/6
- 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]>
2026-09-28 11:10:07 +02:00
Nick SzaunigandClaude Opus 5.5 f263ec79f2 feat(export): Stockkarte - a colony's complete record as PDF
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]>
2026-09-28 06:27:55 +02:00
Nick SzaunigandClaude Opus 5.5 5cf4e87ca3 feat(automation): suggest follow-up checks after treatments and feedings
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]>
2026-09-28 06:23:13 +02:00
Nick SzaunigandClaude Opus 5.5 888edf7e1d feat(labels): cut marks in the sheet margin for trimming with a paper cutter
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]>
2026-09-28 06:11:03 +02:00
Nick SzaunigandClaude Opus 5.5 445fb3b94b feat: revocable read-only API keys for machine access
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]>
2026-09-28 01:24:17 +02:00
Nick SzaunigandClaude Opus 5.5 ede86c062a feat: dedicated, versioned shop-sync feed for Wabenhain
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]>
2026-09-27 20:31:46 +02:00
Nick SzaunigandClaude Opus 5.5 3c7ff1e0b7 docs: document field ownership between ApiCore and Wabenhain
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:15:02 +02:00
Nick SzaunigandClaude Opus 5.5 2d4da96ad8 docs: record review findings, verification state and new architecture decisions
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 13:01:47 +02:00
Nick SzaunigandClaude Opus 5.5 f9374a135a feat: "Heute" start page with one next step, clear task states and motion levels
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]>
2026-09-27 13:00:44 +02:00
Nick SzaunigandClaude Opus 5.5 5c2ea7bf1e fix: resend offline-queued entries with an idempotency key
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]>
2026-09-27 05:31:04 +02:00
Nick SzaunigandClaude Opus 5.5 5d11756a87 fix: keep quantities, colony/queen states and tasks consistent under retries
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]>
2026-09-27 05:27:45 +02:00
Nick SzaunigandClaude Opus 5 786d929df9 feat: expose the remaining fields the edit forms were withholding
Backend Image / build-and-push (push) Successful in 2m11s
Frontend Image / build-and-push (push) Successful in 1m53s
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]>
2026-09-22 23:11:12 +02:00
Nick SzaunigandClaude Opus 5 5a5bd4a138 fix: print the jar size on labels in grams, not millilitres
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]>
2026-09-22 02:37:00 +02:00
Nick SzaunigandClaude Opus 5 46a2322814 fix: restore the e2e suite, which could not compile at all
Backend Image / build-and-push (push) Successful in 2m4s
Frontend Image / build-and-push (push) Successful in 1m51s
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]>
2026-09-22 02:22:09 +02:00
Nick SzaunigandClaude Opus 5 b8809eeaa2 fix: restore the authorization decorators three new routes displaced
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]>
2026-09-22 02:12:56 +02:00
Nick SzaunigandClaude Opus 5 9a147ee123 feat: make webhooks and documents editable instead of delete-and-recreate
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]>
2026-09-22 02:07:48 +02:00
Nick SzaunigandClaude Opus 5 42ceb33844 feat: allow correcting honey measurements and queen evaluations
Backend Image / build-and-push (push) Successful in 2m3s
Frontend Image / build-and-push (push) Successful in 1m41s
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]>
2026-09-19 08:15:54 +02:00
Nick SzaunigandClaude Opus 5 b0e05daa42 feat: open the last locked fields for editing, keeping their history intact
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]>
2026-09-19 08:13:00 +02:00