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:
1 parent
10cbe9bf2d
commit
c01a38835a
340 files changed
+25695
-1808
No files matched your search
@@ -78,8 +78,8 @@ model User {
|
||||
|
||||
// TOTP-Secret AES-256-GCM-verschluesselt gespeichert (siehe
|
||||
// src/auth/mfa/mfa-crypto.util.ts), nie im Klartext in der DB.
|
||||
mfaEnabled Boolean @default(false)
|
||||
mfaSecret String?
|
||||
mfaEnabled Boolean @default(false)
|
||||
mfaSecret String?
|
||||
// Zuletzt angenommener TOTP-Zeitschritt: derselbe Code (oder ein aelterer)
|
||||
// wird danach nie wieder akzeptiert (Replay-Schutz).
|
||||
mfaLastTimeStep Int?
|
||||
@@ -88,7 +88,7 @@ model User {
|
||||
// ab 5 ist das Konto bis authLockedUntil gesperrt.
|
||||
failedAuthAttempts Int @default(0)
|
||||
authLockedUntil DateTime?
|
||||
avatarPath String?
|
||||
avatarPath String?
|
||||
|
||||
// Gesetzt bei jedem Passwortwechsel; JwtAuthGuard verwirft Access-Tokens,
|
||||
// die vor diesem Zeitpunkt ausgestellt wurden (siehe
|
||||
@@ -170,16 +170,16 @@ model ConsumedToken {
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
// Anmeldezeitpunkt der Sitzung, an jede rotierte Nachfolge-Token vererbt:
|
||||
// spaetestens SESSION_MAX_AGE danach ist neu anzumelden.
|
||||
sessionStartedAt DateTime @default(now())
|
||||
sessionStartedAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
@@ -223,23 +223,25 @@ enum ApiaryStatus {
|
||||
}
|
||||
|
||||
model Apiary {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
location String?
|
||||
latitude Float?
|
||||
longitude Float?
|
||||
address String?
|
||||
status ApiaryStatus @default(ACTIVE)
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deactivatedAt DateTime?
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
location String?
|
||||
latitude Float?
|
||||
longitude Float?
|
||||
address String?
|
||||
// Coarse, public-safe place name for storefront/shop output (never the exact address).
|
||||
publicLocationLabel String?
|
||||
status ApiaryStatus @default(ACTIVE)
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deactivatedAt DateTime?
|
||||
|
||||
colonies Colony[]
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
harvests Harvest[]
|
||||
treatments Treatment[]
|
||||
colonies Colony[]
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
harvests Harvest[]
|
||||
treatments Treatment[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
|
||||
@@index([status])
|
||||
@@ -454,6 +456,7 @@ model Inspection {
|
||||
weather String?
|
||||
temperature Float?
|
||||
colonyStrength Int?
|
||||
broodFrames Int?
|
||||
broodAmount String?
|
||||
broodPattern Int?
|
||||
openBroodPresent Boolean?
|
||||
@@ -761,8 +764,8 @@ model Harvest {
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
colonies HarvestColony[]
|
||||
extractions ExtractionHarvest[]
|
||||
colonies HarvestColony[]
|
||||
extractions ExtractionHarvest[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
|
||||
@@index([apiaryId, date])
|
||||
@@ -1300,6 +1303,8 @@ model WebhookSubscription {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
deliveries WebhookDelivery[]
|
||||
|
||||
@@index([active])
|
||||
}
|
||||
|
||||
@@ -1403,6 +1408,8 @@ model SiteSettings {
|
||||
// part of any public page.
|
||||
holderName String?
|
||||
holderAddress String?
|
||||
// Country of origin printed on labels and shop output.
|
||||
originCountry String @default("Deutschland")
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -1521,3 +1528,119 @@ model CalendarSyncOutboxEntry {
|
||||
@@unique([sourceType, sourceId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plattform-Rückgrat: Domain-Events (Outbox), Webhook-Zustellung, Job-Läufe,
|
||||
// Throttler-Zähler. Alle fünf Modelle sind vom Backup ausgenommen.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum DomainEventStatus {
|
||||
PENDING
|
||||
PROCESSING
|
||||
DONE
|
||||
FAILED
|
||||
}
|
||||
|
||||
model DomainEvent {
|
||||
id String @id @default(cuid())
|
||||
type String
|
||||
aggregateType String
|
||||
aggregateId String
|
||||
payload Json
|
||||
actorUserId String?
|
||||
correlationId String
|
||||
// Nullable @unique instead of a partial index: avoids the migration drift
|
||||
// described in the backbone spec (section 3.5).
|
||||
dedupeKey String? @unique
|
||||
occurredAt DateTime @default(now())
|
||||
status DomainEventStatus @default(PENDING)
|
||||
attempts Int @default(0)
|
||||
availableAt DateTime @default(now())
|
||||
claimedAt DateTime?
|
||||
processedAt DateTime?
|
||||
lastError String?
|
||||
|
||||
deliveries DomainEventDelivery[]
|
||||
|
||||
@@index([status, availableAt])
|
||||
@@index([correlationId])
|
||||
@@index([aggregateType, aggregateId])
|
||||
}
|
||||
|
||||
enum DomainEventDeliveryStatus {
|
||||
PENDING
|
||||
DONE
|
||||
FAILED
|
||||
}
|
||||
|
||||
model DomainEventDelivery {
|
||||
id String @id @default(cuid())
|
||||
eventId String
|
||||
event DomainEvent @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
handler String
|
||||
status DomainEventDeliveryStatus @default(PENDING)
|
||||
attempts Int @default(0)
|
||||
lastError String?
|
||||
completedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([eventId, handler])
|
||||
}
|
||||
|
||||
enum WebhookDeliveryStatus {
|
||||
PENDING
|
||||
PROCESSING
|
||||
DELIVERED
|
||||
FAILED
|
||||
}
|
||||
|
||||
model WebhookDelivery {
|
||||
id String @id @default(cuid())
|
||||
subscriptionId String
|
||||
subscription WebhookSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
event String
|
||||
body String
|
||||
status WebhookDeliveryStatus @default(PENDING)
|
||||
attempts Int @default(0)
|
||||
availableAt DateTime @default(now())
|
||||
claimedAt DateTime?
|
||||
lastStatusCode Int?
|
||||
lastError String?
|
||||
deliveredAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([status, availableAt])
|
||||
@@index([subscriptionId, createdAt])
|
||||
}
|
||||
|
||||
enum JobRunStatus {
|
||||
RUNNING
|
||||
SUCCEEDED
|
||||
FAILED
|
||||
}
|
||||
|
||||
model JobRun {
|
||||
id String @id @default(cuid())
|
||||
job String
|
||||
status JobRunStatus @default(RUNNING)
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
result Json?
|
||||
error String?
|
||||
|
||||
@@index([job, startedAt])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// UNLOGGED in the migration: no WAL, emptied after a crash - only counters get lost.
|
||||
model ThrottlerHit {
|
||||
key String
|
||||
throttlerName String
|
||||
hits Int
|
||||
expiresAt DateTime
|
||||
blockedUntil DateTime?
|
||||
|
||||
@@id([key, throttlerName])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
Reference in new issue
Block a user