feat(nextcloud-status): Benachrichtigung abonnieren und Mail bei Störung
- Glocke je Kachel (persönlich, Benutzen-Ebene), Tabelle NextcloudAlertSubscription mit RLS - Zwei-Fehlschläge-Regel und gemeldeter Zustand auf der Zeile, Anspruch vor dem Mailversand - Mail (nur Deutsch) über MailService, bis zu drei Versuche, Zugriff beim Senden erneut geprüft - Fehlercodes in der Mail als lesbarer Hinweis Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import type {
|
||||
CreateNextcloudInstanceDto,
|
||||
UpdateNextcloudInstanceDto,
|
||||
} from './dto/nextcloud-instance.dto';
|
||||
import { NextcloudAlertService } from './nextcloud-alert.service';
|
||||
import { planStatusWrite } from './nextcloud-alert-rules';
|
||||
import { checkLogoUpload } from './nextcloud-logo-rules';
|
||||
import {
|
||||
type NextcloudRating,
|
||||
@@ -74,6 +76,12 @@ export interface NextcloudInstanceView {
|
||||
errorDetail: string | null;
|
||||
};
|
||||
rating: NextcloudRating;
|
||||
/**
|
||||
* Glocke des anfragenden Benutzers (quick-261002-kxc, D-K10). Nur in den
|
||||
* Listenantworten gesetzt; Einzelantworten lassen das Feld weg — die Seite
|
||||
* behaelt dann den Stand der Kachel.
|
||||
*/
|
||||
subscribed?: boolean;
|
||||
}
|
||||
|
||||
export interface NextcloudListView {
|
||||
@@ -112,6 +120,7 @@ export class NextcloudStatusService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly release: NextcloudReleaseService,
|
||||
private readonly alerts: NextcloudAlertService,
|
||||
) {}
|
||||
|
||||
private toView(row: PublicRow, reference: NextcloudReference | null): NextcloudInstanceView {
|
||||
@@ -137,19 +146,26 @@ export class NextcloudStatusService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Alle Clouds des Mandanten samt Bewertung zum Lesezeitpunkt (D-B). */
|
||||
async listForTenant(tenantId: string): Promise<NextcloudListView> {
|
||||
/**
|
||||
* Alle Clouds des Mandanten samt Bewertung zum Lesezeitpunkt (D-B) und dem
|
||||
* Glockenstand des anfragenden Benutzers (D-K10).
|
||||
*/
|
||||
async listForTenant(tenantId: string, userId: string): Promise<NextcloudListView> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const [rows, reference] = await Promise.all([
|
||||
const [rows, reference, subscribed] = await Promise.all([
|
||||
tenantPrisma.nextcloudInstance.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { customerName: 'asc' },
|
||||
select: PUBLIC_SELECT,
|
||||
}),
|
||||
this.release.getReference(),
|
||||
this.alerts.subscribedInstanceIds(tenantId, userId),
|
||||
]);
|
||||
return {
|
||||
instances: rows.map((row) => this.toView(row as PublicRow, reference)),
|
||||
instances: rows.map((row) => ({
|
||||
...this.toView(row as PublicRow, reference),
|
||||
subscribed: subscribed.has((row as PublicRow).id),
|
||||
})),
|
||||
reference: {
|
||||
newestVersion: newestVersion(reference),
|
||||
fetchedAt: reference?.fetchedAt ?? null,
|
||||
@@ -180,33 +196,53 @@ export class NextcloudStatusService {
|
||||
/**
|
||||
* Prueft eine Cloud (nur `status.php`, siehe `fetchNextcloudStatus`) und
|
||||
* schreibt das Ergebnis an die Zeile. Fremde oder unbekannte Kennung: 404.
|
||||
*
|
||||
* Einziger Schreibweg fuer jede Pruefung (stuendlich, Wiederholung, "Jetzt
|
||||
* pruefen", Anlegen, Adressaenderung). `planStatusWrite` setzt die
|
||||
* Zwei-Fehlschlaege-Regel um (ein erster Fehlschlag laesst den gespeicherten
|
||||
* Zustand unveraendert), danach entscheidet `evaluateAfterCheck` ueber eine
|
||||
* Meldung — es wartet nur auf den Anspruch, nie auf den Mailversand.
|
||||
*/
|
||||
async checkInstance(tenantId: string, id: string): Promise<NextcloudInstanceView> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const existing = await tenantPrisma.nextcloudInstance.findFirst({
|
||||
where: { id, tenantId },
|
||||
select: { id: true, baseUrl: true },
|
||||
select: { id: true, baseUrl: true, consecutiveFailures: true },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('Cloud nicht gefunden');
|
||||
|
||||
const result = await fetchNextcloudStatus(existing.baseUrl);
|
||||
const now = new Date();
|
||||
const plan = planStatusWrite(existing.consecutiveFailures, result, now);
|
||||
const updated = await tenantPrisma.nextcloudInstance.update({
|
||||
where: { id },
|
||||
data: {
|
||||
reachable: result.reachable,
|
||||
maintenance: result.maintenance,
|
||||
needsDbUpgrade: result.needsDbUpgrade,
|
||||
versionString: result.versionString,
|
||||
edition: result.edition,
|
||||
productName: result.productName,
|
||||
errorKind: result.errorKind,
|
||||
errorDetail: result.errorDetail,
|
||||
lastCheckedAt: new Date(),
|
||||
},
|
||||
select: PUBLIC_SELECT,
|
||||
data: plan.data,
|
||||
select: { ...PUBLIC_SELECT, alertState: true },
|
||||
});
|
||||
return this.toView(updated as PublicRow, await this.release.getReference());
|
||||
const view = this.toView(updated as PublicRow, await this.release.getReference());
|
||||
try {
|
||||
await this.alerts.evaluateAfterCheck(
|
||||
tenantId,
|
||||
{
|
||||
id,
|
||||
customerName: updated.customerName,
|
||||
baseUrl: updated.baseUrl,
|
||||
errorKind: updated.errorKind,
|
||||
errorDetail: updated.errorDetail,
|
||||
alertState: updated.alertState,
|
||||
},
|
||||
view.rating,
|
||||
now,
|
||||
);
|
||||
} catch (err) {
|
||||
// Eine Stoerung der Meldung darf das Pruefergebnis nicht verwerfen.
|
||||
this.logger.error(
|
||||
`Nextcloud-Meldung nach Pruefung fehlgeschlagen (Cloud ${id}): ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aendert Name, Adresse und/oder Logo-Adresse. Eine neue Adresse wird
|
||||
* normalisiert und sofort neu geprueft, eine unveraenderte nicht. Eine
|
||||
@@ -347,7 +383,7 @@ export class NextcloudStatusService {
|
||||
* hoechstens vier gleichzeitig, danach die frische Liste. Eine fehlerhafte
|
||||
* Cloud stoppt die anderen nicht.
|
||||
*/
|
||||
async checkAllForTenant(tenantId: string): Promise<NextcloudListView> {
|
||||
async checkAllForTenant(tenantId: string, userId: string): Promise<NextcloudListView> {
|
||||
const ids = await this.listInstanceIdsForTenant(tenantId);
|
||||
await runWithConcurrency(ids, CHECK_CONCURRENCY, async (id) => {
|
||||
try {
|
||||
@@ -358,7 +394,7 @@ export class NextcloudStatusService {
|
||||
);
|
||||
}
|
||||
});
|
||||
return this.listForTenant(tenantId);
|
||||
return this.listForTenant(tenantId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user