feat: Datenmodell und Umgebung fuer Integrationen

Ein Zustand je Quelle haelt fest, wie weit gelesen wurde und ob es
klappt. Der Merker ist bewusst ein freier Text: Zammad braucht darin
einen Zeitstempel, Telegram den Zaehler der zuletzt geholten Nachricht.

Dazu die gespiegelte Belegung ohne Titel, das Adressbuch der erlaubten
Telegram-Chats und der Einmalcode, mit dem sich ein Chat anmeldet.

Welche Integration eine Instanz darf, entscheidet weiterhin INSTANZ. Ein
Zammad-Token in der Instanz daheim bleibt wirkungslos — das steht in den
Faehigkeiten, nicht in einer Einstellung, die sich verstellen liesse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 23:03:32 +02:00
co-authored by Claude Opus 5
parent ab1b898e44
commit 7f8f505126
5 changed files with 233 additions and 4 deletions
@@ -0,0 +1,62 @@
-- AlterTable
ALTER TABLE "aufgabe" ADD COLUMN "spiegeln" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "benachrichtigungseinstellung" ADD COLUMN "perTelegram" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "integrationszustand" (
"id" TEXT NOT NULL,
"quelle" "Quelle" NOT NULL,
"merker" TEXT,
"letzterLauf" TIMESTAMP(3),
"letzterErfolg" TIMESTAMP(3),
"letzterFehler" TEXT,
"fehlversuche" INTEGER NOT NULL DEFAULT 0,
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"aktualisiertAm" TIMESTAMP(3) NOT NULL,
CONSTRAINT "integrationszustand_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "spiegelbelegung" (
"id" TEXT NOT NULL,
"quelleRef" TEXT NOT NULL,
"beginn" TIMESTAMP(3) NOT NULL,
"ende" TIMESTAMP(3) NOT NULL,
"empfangenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "spiegelbelegung_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "telegram_chat" (
"id" TEXT NOT NULL,
"chatId" BIGINT NOT NULL,
"bezeichnung" TEXT,
"benutzerId" TEXT,
"meldungen" BOOLEAN NOT NULL DEFAULT true,
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"zuletztBenutzt" TIMESTAMP(3),
CONSTRAINT "telegram_chat_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "integrationszustand_quelle_key" ON "integrationszustand"("quelle");
-- CreateIndex
CREATE UNIQUE INDEX "spiegelbelegung_quelleRef_key" ON "spiegelbelegung"("quelleRef");
-- CreateIndex
CREATE INDEX "spiegelbelegung_beginn_idx" ON "spiegelbelegung"("beginn");
-- CreateIndex
CREATE UNIQUE INDEX "telegram_chat_chatId_key" ON "telegram_chat"("chatId");
-- CreateIndex
CREATE INDEX "telegram_chat_benutzerId_idx" ON "telegram_chat"("benutzerId");
-- AddForeignKey
ALTER TABLE "telegram_chat" ADD CONSTRAINT "telegram_chat_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "telegram_kopplung" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"benutzerId" TEXT NOT NULL,
"gueltigBis" TIMESTAMP(3) NOT NULL,
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "telegram_kopplung_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "telegram_kopplung_code_key" ON "telegram_kopplung"("code");
-- CreateIndex
CREATE INDEX "telegram_kopplung_gueltigBis_idx" ON "telegram_kopplung"("gueltigBis");
+93 -3
View File
@@ -38,6 +38,7 @@ model Benutzer {
benachrichtigung Benachrichtigungseinstellung?
pushGeraete PushGeraet[]
meldungen Meldungsnachweis[]
telegramChats TelegramChat[]
@@index([aktiv])
@@map("benutzer")
@@ -222,6 +223,10 @@ model Aufgabe {
/// Kennung beim Ursprungssystem, z. B. die Ticketnummer.
quelleRef String?
/// Ausdruecklich zum Spiegeln freigegeben. Nur was hier true ist, verlaesst
/// die Instanz mit Titel – alles andere bleibt, wo es ist.
spiegeln Boolean @default(false)
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
@@ -391,9 +396,10 @@ model Benachrichtigungseinstellung {
benutzerId String @unique
/// Kommagetrennte Anlaesse, siehe src/lib/benachrichtigung.ts.
anlaesse String @default("tagesueberblick,faelligkeit")
perPush Boolean @default(false)
perMail Boolean @default(false)
anlaesse String @default("tagesueberblick,faelligkeit")
perPush Boolean @default(false)
perMail Boolean @default(false)
perTelegram Boolean @default(false)
/// Alles als Minuten seit Mitternacht, Europe/Berlin.
ueberblickUm Int @default(420)
@@ -448,3 +454,87 @@ model Meldungsnachweis {
@@index([gesendetAm])
@@map("meldungsnachweis")
}
/// Zustand einer Integration: wie weit sie gelesen hat und ob es klappt.
///
/// Eine Zeile je Quelle. Der Merker ist bewusst ein freier Text – jede
/// Integration legt darin ab, was sie zum Fortsetzen braucht: Zammad einen
/// Zeitstempel, Telegram den Zaehler der zuletzt geholten Nachricht.
model Integrationszustand {
id String @id @default(cuid())
quelle Quelle @unique
/// Woher es weitergeht. Bedeutung haengt an der Quelle.
merker String?
letzterLauf DateTime?
letzterErfolg DateTime?
letzterFehler String?
/// Fehlschlaege in Folge; bei Erfolg wieder null.
fehlversuche Int @default(0)
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
@@map("integrationszustand")
}
/// Dienstliche Belegung, die aus der anderen Instanz kommt.
///
/// Bewusst ohne Titel: Daheim soll sehen, *dass* die Zeit weg ist, nicht
/// woran gearbeitet wird. Firmeninhalte verlassen die Firma nicht.
model Spiegelbelegung {
id String @id @default(cuid())
/// Kennung des Zeitblocks in der sendenden Instanz.
quelleRef String @unique
beginn DateTime
ende DateTime
empfangenAm DateTime @default(now())
@@index([beginn])
@@map("spiegelbelegung")
}
/// Ein Telegram-Chat, der mit dem Planer reden darf.
///
/// Wer nicht hier steht, wird verworfen – ein Bot-Token laesst sich erraten
/// oder abgreifen, die Chatkennung ist der zweite Riegel.
model TelegramChat {
id String @id @default(cuid())
/// Chatkennung bei Telegram; kann negativ sein (Gruppen).
chatId BigInt @unique
bezeichnung String?
benutzerId String?
/// Empfaengt dieser Chat auch Meldungen, nicht nur Aufgaben?
meldungen Boolean @default(true)
erstelltAm DateTime @default(now())
zuletztBenutzt DateTime?
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull)
@@index([benutzerId])
@@map("telegram_chat")
}
/// Einmalcode, mit dem sich ein Telegram-Chat anmeldet.
///
/// Ohne ihn muesste die Chatkennung von Hand eingetragen werden – und um sie
/// zu erfahren, muesste der Planer auf Nachrichten Unbekannter antworten.
/// Genau das soll er nicht.
model TelegramKopplung {
id String @id @default(cuid())
code String @unique
benutzerId String
gueltigBis DateTime
erstelltAm DateTime @default(now())
@@index([gueltigBis])
@@map("telegram_kopplung")
}