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[]
|
||||
perPush: boolean
|
||||
perMail: boolean
|
||||
perTelegram: boolean
|
||||
/** Uhrzeit des Tagesüberblicks, Minuten seit Mitternacht. */
|
||||
ueberblickUm: number
|
||||
/** Ruhezeit: von hier bis … nichts melden. */
|
||||
@@ -68,6 +69,7 @@ export const VOREINSTELLUNG: Einstellung = {
|
||||
anlaesse: ['tagesueberblick', 'faelligkeit'],
|
||||
perPush: false,
|
||||
perMail: false,
|
||||
perTelegram: false,
|
||||
ueberblickUm: 7 * 60,
|
||||
ruheVon: 20 * 60,
|
||||
ruheBis: 7 * 60,
|
||||
@@ -103,6 +105,13 @@ export type Meldung = {
|
||||
text: string
|
||||
/** Wohin der Klick führt. */
|
||||
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)) {
|
||||
return { senden: false, grund: 'abgeschaltet' }
|
||||
}
|
||||
if (!einstellung.perPush && !einstellung.perMail) {
|
||||
if (!einstellung.perPush && !einstellung.perMail && !einstellung.perTelegram) {
|
||||
return { senden: false, grund: 'kein-weg' }
|
||||
}
|
||||
if (bereitsGemeldet.has(meldung.schluessel)) {
|
||||
@@ -202,6 +211,7 @@ export const einstellungSchema = z.object({
|
||||
anlaesse: z.array(z.enum(ANLAESSE)).default([]),
|
||||
perPush: z.coerce.boolean().default(false),
|
||||
perMail: z.coerce.boolean().default(false),
|
||||
perTelegram: z.coerce.boolean().default(false),
|
||||
ueberblickUm: uhrzeit,
|
||||
ruheVon: uhrzeit,
|
||||
ruheBis: uhrzeit,
|
||||
|
||||
@@ -17,9 +17,13 @@ import {
|
||||
tagesBeginn,
|
||||
tagesEnde,
|
||||
} from '@/lib/zeit'
|
||||
import type { Quelle } from '@/generated/prisma/enums'
|
||||
import { INTEGRATION_FEHLER_AB_VERSUCH } from '@/lib/integration'
|
||||
import { db } from '@/server/db'
|
||||
import { mailSenden } from '@/server/benachrichtigung/mail'
|
||||
import { pushSenden } from '@/server/benachrichtigung/push'
|
||||
import { telegramMeldung } from '@/server/integration/telegram'
|
||||
import { umgebung } from '@/server/umgebung'
|
||||
import { jetzt } from '@/server/zeitgeber'
|
||||
|
||||
/**
|
||||
@@ -46,6 +50,7 @@ export function alsEinstellung(
|
||||
anlaesse: string
|
||||
perPush: boolean
|
||||
perMail: boolean
|
||||
perTelegram: boolean
|
||||
ueberblickUm: number
|
||||
ruheVon: number
|
||||
ruheBis: number
|
||||
@@ -66,6 +71,7 @@ export function alsEinstellung(
|
||||
anlaesse,
|
||||
perPush: zeile.perPush,
|
||||
perMail: zeile.perMail,
|
||||
perTelegram: zeile.perTelegram,
|
||||
ueberblickUm: zeile.ueberblickUm,
|
||||
ruheVon: zeile.ruheVon,
|
||||
ruheBis: zeile.ruheBis,
|
||||
@@ -86,6 +92,7 @@ async function empfaengerLaden(): Promise<BenutzerMitEinstellung[]> {
|
||||
anlaesse: true,
|
||||
perPush: true,
|
||||
perMail: true,
|
||||
perTelegram: true,
|
||||
ueberblickUm: true,
|
||||
ruheVon: true,
|
||||
ruheBis: true,
|
||||
@@ -141,6 +148,9 @@ export async function meldungZustellen(
|
||||
|
||||
if (benutzer.einstellung.perPush) await pushSenden(benutzer.id, meldung)
|
||||
if (benutzer.einstellung.perMail) await mailSenden(benutzer.email, meldung)
|
||||
if (benutzer.einstellung.perTelegram && umgebung().telegramAktiv) {
|
||||
await telegramMeldung(benutzer.id, meldung)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -232,6 +242,8 @@ async function faelligkeitenBauen(zeitpunkt: Date): Promise<Meldung[]> {
|
||||
titel: 'Heute fällig',
|
||||
text: aufgabe.titel,
|
||||
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(
|
||||
benutzerId: string,
|
||||
zeitpunkt: Date,
|
||||
@@ -283,14 +303,34 @@ async function abgleichfehlerBauen(
|
||||
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)
|
||||
return konten.map((konto) => ({
|
||||
anlass: 'abgleichfehler' as const,
|
||||
schluessel: meldungsSchluessel('abgleichfehler', konto.id, tag),
|
||||
titel: 'Ein Kalender lässt sich nicht abgleichen',
|
||||
text: `„${konto.anzeigename}": ${konto.letzterFehler ?? 'unbekannter Fehler'}`,
|
||||
pfad: '/kalenderkonten',
|
||||
}))
|
||||
return [
|
||||
...konten.map((konto) => ({
|
||||
anlass: 'abgleichfehler' as const,
|
||||
schluessel: meldungsSchluessel('abgleichfehler', konto.id, tag),
|
||||
titel: 'Ein Kalender lässt sich nicht abgleichen',
|
||||
text: `„${konto.anzeigename}": ${konto.letzterFehler ?? 'unbekannter Fehler'}`,
|
||||
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 }
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
alteNachweiseEntfernen,
|
||||
benachrichtigungenVerschicken,
|
||||
} 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'
|
||||
|
||||
/**
|
||||
@@ -31,6 +35,24 @@ const AUFRAEUM_PLAN = '17 3 * * *'
|
||||
* dank Wiederholungssperre nur einmal gemeldet.
|
||||
*/
|
||||
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> {
|
||||
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, () => {
|
||||
void Promise.all([
|
||||
alteTermineEntfernen(),
|
||||
abgelaufeneChallengesEntfernen(jetzt()),
|
||||
alteNachweiseEntfernen(jetzt()),
|
||||
alteKopplungenEntfernen(jetzt()),
|
||||
])
|
||||
.then(() => undefined)
|
||||
.catch((fehler: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user