feat: platform backbone and field capture, first part (checkpoint)

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]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-13 17:57:54 +02:00
1 parent 10cbe9bf2d
commit c01a38835a
340 files changed
+25695 -1808

No files matched your search

@@ -0,0 +1,35 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: apicore-backend-config
data:
PORT: "3001"
JWT_ACCESS_EXPIRES_IN: "15m"
JWT_REFRESH_EXPIRES_IN: "7d"
FRONTEND_ORIGIN: "https://apicore.example.com"
DOCUMENTS_STORAGE_PATH: "/data/documents"
# Automatische Backups. Ohne BACKUP_STORAGE_PATH bleibt der Zeitplan
# deaktiviert. Bei >1 Replica muss dies auf ein geteiltes Volume zeigen,
# wie DOCUMENTS_STORAGE_PATH.
BACKUP_STORAGE_PATH: "/data/backups"
BACKUP_CRON_SCHEDULE: "0 3 * * *"
BACKUP_RETENTION_COUNT: "14"
# Optional: groesste einspielbare Backup-Datei in Bytes (Standard 50 MB,
# hoechstens 256 MB). Export und Zeitplan warnen, sobald ein Backup
# groesser wird. Ein Body-Limit am Ingress muss mitgezogen werden.
# BACKUP_MAX_IMPORT_BYTES: "52428800"
COOKIE_SECURE: "true"
# Client-IP fuer Rate-Limits und IP-Freigaben der API-Schluessel. Vertraut
# werden nur das Pod-Netz 10.42.0.0/16 (Traefik) und Cloudflare
# (https://www.cloudflare.com/ips/), bewusst nicht LAN/uniquelocal: LAN-Clients
# sind selbst Absender, ihre X-Forwarded-For-Werte verwirft Traefik.
# Express nimmt den ersten X-Forwarded-For-Eintrag von rechts, der aus keinem
# dieser Netze kommt: den Client, den Cloudflare angehaengt hat. Mit "1" waere
# es die Cloudflare-Edge, die sich viele Clients teilen.
TRUST_PROXY: "loopback, 10.42.0.0/16, 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22, 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20, 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13, 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22, 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32, 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32"
# Entra ID SSO. Issuer ist die v2.0-Metadaten-URL des Tenants,
# Redirect-URI muss exakt der im App-Registrierung hinterlegten entsprechen.
# Ohne diese vier Variablen (siehe secret.example.yaml fuer Client-ID/-Secret)
# bleibt SSO deaktiviert, /auth/sso/status meldet dann enabled: false.
OIDC_ISSUER_URL: "https://login.microsoftonline.com/<tenant-id>/v2.0"
OIDC_REDIRECT_URI: "https://api.apicore.example.com/api/v1/auth/sso/callback"
+15
View File
@@ -0,0 +1,15 @@
# Persistenter Speicher fuer Dokumente, Avatare und die verschluesselten
# App-Backups (ConfigMap: DOCUMENTS_STORAGE_PATH=/data/documents,
# BACKUP_STORAGE_PATH=/data/backups). Im Cluster: StorageClass smb-app
# (SMB 3.1.1 mit seal, uid/gid 1000 = User "node" aus dem Dockerfile).
# ReadWriteMany, damit mehrere Backend-Replicas dieselben Dateien sehen.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: apicore-data
spec:
accessModes: [ReadWriteMany]
storageClassName: smb-app
resources:
requests:
storage: 20Gi
+88
View File
@@ -0,0 +1,88 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: apicore-backend
labels:
app: apicore-backend
spec:
# Ohne HPA genau ein Pod. components/api-hpa entfernt replicas, dann
# steuert der HPA (1-2 Pods).
replicas: 1
# Erst der neue Pod, dann weg mit dem alten: scheitert die Migration im
# initContainer (components/backend-roles), laeuft der alte Pod weiter.
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: apicore-backend
template:
metadata:
labels:
app: apicore-backend
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
volumes:
# Dokumente, Avatare und App-Backups (DOCUMENTS_STORAGE_PATH /
# BACKUP_STORAGE_PATH unter /data). Ohne Volume landen sie im
# Container und sind beim naechsten Neustart weg.
- name: data
persistentVolumeClaim:
claimName: apicore-data
containers:
- name: apicore-backend
# Ohne Tag: den Digest setzt das Overlay (images in
# overlays/encry/kustomization.yaml), nie :latest - sonst gibt es
# kein Rollback und jeder Neustart zieht ungeprueft ein neues Image.
image: git.encry.de/nick.rosenloecher/apicore-backend
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /data
ports:
- containerPort: 3001
envFrom:
- configMapRef:
name: apicore-backend-config
- secretRef:
name: apicore-backend-secret
# startupProbe gibt dem Start bis zu 5 Minuten, ohne dass
# liveness/readiness in der Zwischenzeit einen Neustart ausloesen.
# Das alte Image fuehrt `prisma migrate deploy` noch im CMD aus, ab
# components/backend-roles laeuft die Migration im initContainer.
startupProbe:
httpGet:
path: /health/ready
port: 3001
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /health/ready
port: 3001
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: "0"
memory: "0"
limits:
cpu: 500m
memory: 512Mi
@@ -0,0 +1,39 @@
# Vorlage – keine echten Werte. Kopieren, echte Werte setzen, per `kubectl apply` außerhalb
# der Versionskontrolle anwenden (oder ueber Secret-Manager/Sealed-Secrets im Cluster).
apiVersion: v1
kind: Secret
metadata:
name: apicore-backend-secret
type: Opaque
stringData:
# Host ist der In-Cluster-Service aus deploy/k8s/base/postgres/service.yaml.
# Anderer Namespace als das Backend? Dann FQDN nutzen:
# apicore-postgres.<namespace>.svc.cluster.local
DATABASE_URL: "postgresql://apicore:change-me-postgres-password@apicore-postgres:5432/apicore?schema=public"
# Mindestens 32 Zeichen (server-seitig erzwungen, Start bricht sonst ab) -
# signiert jedes Access-/MFA-/SSO-Token. TOTP- und Webhook-Secrets aus der
# Zeit vor den eigenen Schluesseln (unten) sind noch damit verschluesselt:
# erst nach dem rekey-Job rotieren. Erzeugen z.B. mit: openssl rand -hex 32
JWT_ACCESS_SECRET: "change-me-to-a-random-value-at-least-32-characters-long"
JWT_REFRESH_SECRET: "change-me-refresh-secret"
# One encryption key per purpose (TOTP secrets, webhook signing secrets,
# backup files). Required in production: at least 32 characters each, all
# three different from each other and from both JWT secrets.
# BACKUP_ENCRYPTION_KEY (with MFA_/WEBHOOK_ENCRYPTION_KEY for MFA and webhook
# secrets) is needed to restore backups on another deployment.
# Rotation: old value to <NAME>_PREVIOUS, new <NAME>, run the rekey job,
# then remove <NAME>_PREVIOUS (backup files are not resealed: keep
# BACKUP_ENCRYPTION_KEY_PREVIOUS while older files must restore).
MFA_ENCRYPTION_KEY: "change-me-to-a-third-random-value-at-least-32-characters"
WEBHOOK_ENCRYPTION_KEY: "change-me-to-a-fourth-random-value-at-least-32-characters"
BACKUP_ENCRYPTION_KEY: "change-me-to-another-random-value-at-least-32-characters"
# MFA_ENCRYPTION_KEY_PREVIOUS: ""
# WEBHOOK_ENCRYPTION_KEY_PREVIOUS: ""
# BACKUP_ENCRYPTION_KEY_PREVIOUS: ""
# Entra ID App-Registrierung (SSO). Siehe configmap.example.yaml
# fuer OIDC_ISSUER_URL/OIDC_REDIRECT_URI. Wird auch fuer die
# M365-Kalender-Synchronisation wiederverwendet - dafuer in der
# App-Registrierung zusaetzlich die Application-Permission
# "Calendars.ReadWrite" hinzufuegen (Admin-Consent noetig).
OIDC_CLIENT_ID: "change-me-entra-client-id"
OIDC_CLIENT_SECRET: "change-me-entra-client-secret"
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: apicore-backend
spec:
selector:
app: apicore-backend
ports:
- port: 80
targetPort: 3001
type: ClusterIP