feat: Integrationen takten und Telegram als dritten Meldeweg
Telegram alle zwei Minuten, Zammad und Spiegel alle fuenfzehn. Wer dem Bot etwas schickt, erwartet die Aufgabe gleich in der Liste; in Zammad aendert sich dagegen nichts im Minutentakt. Meldungen gehen jetzt auch per Telegram hinaus — mit einem Knopf zum Abhaken, wenn es um genau eine Aufgabe geht. Eine klemmende Integration meldet sich wie ein klemmender Kalender: erst nach dem dritten Fehlversuch, dann aber deutlich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6dad3331e2
commit
bc60e5f864
@@ -47,6 +47,7 @@ export type Einstellung = {
|
|||||||
anlaesse: readonly Anlass[]
|
anlaesse: readonly Anlass[]
|
||||||
perPush: boolean
|
perPush: boolean
|
||||||
perMail: boolean
|
perMail: boolean
|
||||||
|
perTelegram: boolean
|
||||||
/** Uhrzeit des Tagesüberblicks, Minuten seit Mitternacht. */
|
/** Uhrzeit des Tagesüberblicks, Minuten seit Mitternacht. */
|
||||||
ueberblickUm: number
|
ueberblickUm: number
|
||||||
/** Ruhezeit: von hier bis … nichts melden. */
|
/** Ruhezeit: von hier bis … nichts melden. */
|
||||||
@@ -68,6 +69,7 @@ export const VOREINSTELLUNG: Einstellung = {
|
|||||||
anlaesse: ['tagesueberblick', 'faelligkeit'],
|
anlaesse: ['tagesueberblick', 'faelligkeit'],
|
||||||
perPush: false,
|
perPush: false,
|
||||||
perMail: false,
|
perMail: false,
|
||||||
|
perTelegram: false,
|
||||||
ueberblickUm: 7 * 60,
|
ueberblickUm: 7 * 60,
|
||||||
ruheVon: 20 * 60,
|
ruheVon: 20 * 60,
|
||||||
ruheBis: 7 * 60,
|
ruheBis: 7 * 60,
|
||||||
@@ -103,6 +105,13 @@ export type Meldung = {
|
|||||||
text: string
|
text: string
|
||||||
/** Wohin der Klick führt. */
|
/** Wohin der Klick führt. */
|
||||||
pfad: string
|
pfad: string
|
||||||
|
/**
|
||||||
|
* Die Aufgabe, um die es geht — sofern es genau eine ist.
|
||||||
|
*
|
||||||
|
* Nur dann lässt sich unter die Meldung ein Knopf hängen, der sie abhakt.
|
||||||
|
* Ein Tagesüberblick hat keine.
|
||||||
|
*/
|
||||||
|
objektId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,7 +133,7 @@ export function sendenEntscheiden(
|
|||||||
if (!einstellung.anlaesse.includes(meldung.anlass)) {
|
if (!einstellung.anlaesse.includes(meldung.anlass)) {
|
||||||
return { senden: false, grund: 'abgeschaltet' }
|
return { senden: false, grund: 'abgeschaltet' }
|
||||||
}
|
}
|
||||||
if (!einstellung.perPush && !einstellung.perMail) {
|
if (!einstellung.perPush && !einstellung.perMail && !einstellung.perTelegram) {
|
||||||
return { senden: false, grund: 'kein-weg' }
|
return { senden: false, grund: 'kein-weg' }
|
||||||
}
|
}
|
||||||
if (bereitsGemeldet.has(meldung.schluessel)) {
|
if (bereitsGemeldet.has(meldung.schluessel)) {
|
||||||
@@ -202,6 +211,7 @@ export const einstellungSchema = z.object({
|
|||||||
anlaesse: z.array(z.enum(ANLAESSE)).default([]),
|
anlaesse: z.array(z.enum(ANLAESSE)).default([]),
|
||||||
perPush: z.coerce.boolean().default(false),
|
perPush: z.coerce.boolean().default(false),
|
||||||
perMail: z.coerce.boolean().default(false),
|
perMail: z.coerce.boolean().default(false),
|
||||||
|
perTelegram: z.coerce.boolean().default(false),
|
||||||
ueberblickUm: uhrzeit,
|
ueberblickUm: uhrzeit,
|
||||||
ruheVon: uhrzeit,
|
ruheVon: uhrzeit,
|
||||||
ruheBis: uhrzeit,
|
ruheBis: uhrzeit,
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ import {
|
|||||||
tagesBeginn,
|
tagesBeginn,
|
||||||
tagesEnde,
|
tagesEnde,
|
||||||
} from '@/lib/zeit'
|
} from '@/lib/zeit'
|
||||||
|
import type { Quelle } from '@/generated/prisma/enums'
|
||||||
|
import { INTEGRATION_FEHLER_AB_VERSUCH } from '@/lib/integration'
|
||||||
import { db } from '@/server/db'
|
import { db } from '@/server/db'
|
||||||
import { mailSenden } from '@/server/benachrichtigung/mail'
|
import { mailSenden } from '@/server/benachrichtigung/mail'
|
||||||
import { pushSenden } from '@/server/benachrichtigung/push'
|
import { pushSenden } from '@/server/benachrichtigung/push'
|
||||||
|
import { telegramMeldung } from '@/server/integration/telegram'
|
||||||
|
import { umgebung } from '@/server/umgebung'
|
||||||
import { jetzt } from '@/server/zeitgeber'
|
import { jetzt } from '@/server/zeitgeber'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +50,7 @@ export function alsEinstellung(
|
|||||||
anlaesse: string
|
anlaesse: string
|
||||||
perPush: boolean
|
perPush: boolean
|
||||||
perMail: boolean
|
perMail: boolean
|
||||||
|
perTelegram: boolean
|
||||||
ueberblickUm: number
|
ueberblickUm: number
|
||||||
ruheVon: number
|
ruheVon: number
|
||||||
ruheBis: number
|
ruheBis: number
|
||||||
@@ -66,6 +71,7 @@ export function alsEinstellung(
|
|||||||
anlaesse,
|
anlaesse,
|
||||||
perPush: zeile.perPush,
|
perPush: zeile.perPush,
|
||||||
perMail: zeile.perMail,
|
perMail: zeile.perMail,
|
||||||
|
perTelegram: zeile.perTelegram,
|
||||||
ueberblickUm: zeile.ueberblickUm,
|
ueberblickUm: zeile.ueberblickUm,
|
||||||
ruheVon: zeile.ruheVon,
|
ruheVon: zeile.ruheVon,
|
||||||
ruheBis: zeile.ruheBis,
|
ruheBis: zeile.ruheBis,
|
||||||
@@ -86,6 +92,7 @@ async function empfaengerLaden(): Promise<BenutzerMitEinstellung[]> {
|
|||||||
anlaesse: true,
|
anlaesse: true,
|
||||||
perPush: true,
|
perPush: true,
|
||||||
perMail: true,
|
perMail: true,
|
||||||
|
perTelegram: true,
|
||||||
ueberblickUm: true,
|
ueberblickUm: true,
|
||||||
ruheVon: true,
|
ruheVon: true,
|
||||||
ruheBis: true,
|
ruheBis: true,
|
||||||
@@ -141,6 +148,9 @@ export async function meldungZustellen(
|
|||||||
|
|
||||||
if (benutzer.einstellung.perPush) await pushSenden(benutzer.id, meldung)
|
if (benutzer.einstellung.perPush) await pushSenden(benutzer.id, meldung)
|
||||||
if (benutzer.einstellung.perMail) await mailSenden(benutzer.email, meldung)
|
if (benutzer.einstellung.perMail) await mailSenden(benutzer.email, meldung)
|
||||||
|
if (benutzer.einstellung.perTelegram && umgebung().telegramAktiv) {
|
||||||
|
await telegramMeldung(benutzer.id, meldung)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -232,6 +242,8 @@ async function faelligkeitenBauen(zeitpunkt: Date): Promise<Meldung[]> {
|
|||||||
titel: 'Heute fällig',
|
titel: 'Heute fällig',
|
||||||
text: aufgabe.titel,
|
text: aufgabe.titel,
|
||||||
pfad: '/aufgaben',
|
pfad: '/aufgaben',
|
||||||
|
// Genau eine Aufgabe: Telegram kann einen Knopf zum Abhaken anbieten.
|
||||||
|
objektId: aufgabe.id,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +278,15 @@ async function zeitblockErinnerungenBauen(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kalender, die mehrfach hintereinander nicht abgleichbar waren. */
|
/** Lesbare Namen für die Meldung; die Aufzählung selbst ist technisch. */
|
||||||
|
const INTEGRATION_NAME: Partial<Record<Quelle, string>> = {
|
||||||
|
ZAMMAD: 'Zammad',
|
||||||
|
WARTUNGSPLANER: 'Wartungsplaner',
|
||||||
|
TELEGRAM: 'Telegram',
|
||||||
|
SPIEGEL: 'Spiegel',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kalender und Integrationen, die mehrfach hintereinander klemmten. */
|
||||||
async function abgleichfehlerBauen(
|
async function abgleichfehlerBauen(
|
||||||
benutzerId: string,
|
benutzerId: string,
|
||||||
zeitpunkt: Date,
|
zeitpunkt: Date,
|
||||||
@@ -283,14 +303,34 @@ async function abgleichfehlerBauen(
|
|||||||
select: { id: true, anzeigename: true, letzterFehler: true },
|
select: { id: true, anzeigename: true, letzterFehler: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Eine klemmende Integration wiegt genauso schwer wie ein klemmender
|
||||||
|
// Kalender: In beiden Fällen steht die Liste auf einem alten Stand, ohne
|
||||||
|
// dass es jemandem auffiele.
|
||||||
|
const integrationen = await db.integrationszustand.findMany({
|
||||||
|
where: {
|
||||||
|
letzterFehler: { not: null },
|
||||||
|
fehlversuche: { gte: INTEGRATION_FEHLER_AB_VERSUCH },
|
||||||
|
},
|
||||||
|
select: { quelle: true, letzterFehler: true },
|
||||||
|
})
|
||||||
|
|
||||||
const tag = alsTagesschluessel(zeitpunkt)
|
const tag = alsTagesschluessel(zeitpunkt)
|
||||||
return konten.map((konto) => ({
|
return [
|
||||||
|
...konten.map((konto) => ({
|
||||||
anlass: 'abgleichfehler' as const,
|
anlass: 'abgleichfehler' as const,
|
||||||
schluessel: meldungsSchluessel('abgleichfehler', konto.id, tag),
|
schluessel: meldungsSchluessel('abgleichfehler', konto.id, tag),
|
||||||
titel: 'Ein Kalender lässt sich nicht abgleichen',
|
titel: 'Ein Kalender lässt sich nicht abgleichen',
|
||||||
text: `„${konto.anzeigename}": ${konto.letzterFehler ?? 'unbekannter Fehler'}`,
|
text: `„${konto.anzeigename}": ${konto.letzterFehler ?? 'unbekannter Fehler'}`,
|
||||||
pfad: '/kalenderkonten',
|
pfad: '/kalenderkonten',
|
||||||
}))
|
})),
|
||||||
|
...integrationen.map((i) => ({
|
||||||
|
anlass: 'abgleichfehler' as const,
|
||||||
|
schluessel: meldungsSchluessel('abgleichfehler', i.quelle, tag),
|
||||||
|
titel: 'Eine Integration klemmt',
|
||||||
|
text: `${INTEGRATION_NAME[i.quelle] ?? i.quelle}: ${i.letzterFehler ?? 'unbekannter Fehler'}`,
|
||||||
|
pfad: '/integrationen',
|
||||||
|
})),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VersandBericht = { geprueft: number; gesendet: number }
|
export type VersandBericht = { geprueft: number; gesendet: number }
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
alteNachweiseEntfernen,
|
alteNachweiseEntfernen,
|
||||||
benachrichtigungenVerschicken,
|
benachrichtigungenVerschicken,
|
||||||
} from '@/server/benachrichtigung/versand'
|
} from '@/server/benachrichtigung/versand'
|
||||||
|
import { zammadAbgleichen } from '@/server/integration/zammad'
|
||||||
|
import { alteKopplungenEntfernen, telegramAbholen } from '@/server/integration/telegram'
|
||||||
|
import { spiegelSenden } from '@/server/integration/spiegel'
|
||||||
|
import { umgebung } from '@/server/umgebung'
|
||||||
import { jetzt } from '@/server/zeitgeber'
|
import { jetzt } from '@/server/zeitgeber'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +35,24 @@ const AUFRAEUM_PLAN = '17 3 * * *'
|
|||||||
* dank Wiederholungssperre nur einmal gemeldet.
|
* dank Wiederholungssperre nur einmal gemeldet.
|
||||||
*/
|
*/
|
||||||
const MELDUNGS_PLAN = '*/10 * * * *'
|
const MELDUNGS_PLAN = '*/10 * * * *'
|
||||||
|
/**
|
||||||
|
* Integrationen laufen im eigenen Takt.
|
||||||
|
*
|
||||||
|
* Telegram oefter als der Rest: Wer dem Bot etwas schickt, erwartet die
|
||||||
|
* Aufgabe gleich in der Liste, nicht in einer Viertelstunde. Zammad und der
|
||||||
|
* Spiegel duerfen traeger sein — dort aendert sich nichts im Minutentakt.
|
||||||
|
*/
|
||||||
|
const TELEGRAM_PLAN = '*/2 * * * *'
|
||||||
|
const ZAMMAD_PLAN = '*/15 * * * *'
|
||||||
|
const SPIEGEL_PLAN = '*/15 * * * *'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wer im Protokoll steht, wenn niemand geklickt hat.
|
||||||
|
*
|
||||||
|
* Ein Job hat keinen Benutzer. Statt den Eintrag wegzulassen, traegt er
|
||||||
|
* sichtbar den Dienst ein — sonst sieht es aus, als sei nichts passiert.
|
||||||
|
*/
|
||||||
|
const DIENST = { id: null, email: 'dienst@intern' }
|
||||||
|
|
||||||
async function alleKontenAbgleichen(): Promise<void> {
|
async function alleKontenAbgleichen(): Promise<void> {
|
||||||
const zeitpunkt = jetzt()
|
const zeitpunkt = jetzt()
|
||||||
@@ -87,11 +109,53 @@ export function jobsStarten(): void {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const einst = umgebung()
|
||||||
|
|
||||||
|
if (einst.zammadAktiv) {
|
||||||
|
cron.schedule(ZAMMAD_PLAN, () => {
|
||||||
|
void zammadAbgleichen(DIENST)
|
||||||
|
.then((bericht) => {
|
||||||
|
if (bericht.angelegt > 0 || bericht.geschlossen > 0) {
|
||||||
|
console.warn(
|
||||||
|
`Zammad: ${bericht.angelegt} neu, ${bericht.geschlossen} geschlossen.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((fehler: unknown) => {
|
||||||
|
console.error('Zammad-Abgleich abgebrochen:', fehler)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (einst.telegramAktiv) {
|
||||||
|
cron.schedule(TELEGRAM_PLAN, () => {
|
||||||
|
void telegramAbholen(DIENST)
|
||||||
|
.then((bericht) => {
|
||||||
|
if (bericht.verworfen > 0) {
|
||||||
|
// Nie die Chatkennung: Sie gehoert zu einer Person.
|
||||||
|
console.warn(`Telegram: ${bericht.verworfen} Nachricht(en) verworfen.`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((fehler: unknown) => {
|
||||||
|
console.error('Telegram-Abholen abgebrochen:', fehler)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (einst.spiegelSendenAktiv) {
|
||||||
|
cron.schedule(SPIEGEL_PLAN, () => {
|
||||||
|
void spiegelSenden().catch((fehler: unknown) => {
|
||||||
|
console.error('Spiegel-Versand abgebrochen:', fehler)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
cron.schedule(AUFRAEUM_PLAN, () => {
|
cron.schedule(AUFRAEUM_PLAN, () => {
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
alteTermineEntfernen(),
|
alteTermineEntfernen(),
|
||||||
abgelaufeneChallengesEntfernen(jetzt()),
|
abgelaufeneChallengesEntfernen(jetzt()),
|
||||||
alteNachweiseEntfernen(jetzt()),
|
alteNachweiseEntfernen(jetzt()),
|
||||||
|
alteKopplungenEntfernen(jetzt()),
|
||||||
])
|
])
|
||||||
.then(() => undefined)
|
.then(() => undefined)
|
||||||
.catch((fehler: unknown) => {
|
.catch((fehler: unknown) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user