feat(nextcloud-status): erneute Prüfung nach Ausfall und Hinweis auf der Kachel

- Wiederholungsauftrag jede Minute für Clouds mit einem Fehlschlag älter als fünf Minuten
- Neue Adresse setzt den Prüfstand zurück, der gemeldete Zustand bleibt
- Kachel: Hinweis Prüfung fehlgeschlagen, Fehlercodes als lesbarer Text mit Tooltip

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 15:26:23 +02:00
parent faed0d760f
commit 11a70c9ee9
14 changed files with 515 additions and 34 deletions
@@ -39,6 +39,7 @@ export const PUBLIC_SELECT = {
edition: true,
errorKind: true,
errorDetail: true,
consecutiveFailures: true,
} as const;
type PublicRow = {
@@ -56,6 +57,7 @@ type PublicRow = {
edition: string | null;
errorKind: string | null;
errorDetail: string | null;
consecutiveFailures: number;
};
export interface NextcloudInstanceView {
@@ -74,6 +76,11 @@ export interface NextcloudInstanceView {
edition: string | null;
errorKind: string | null;
errorDetail: string | null;
/**
* Genau ein Fehlschlag in Folge: die Kachel zeigt den letzten guten Stand
* mit Hinweis, die Wiederholung folgt in wenigen Minuten (L-03).
*/
pendingRetry: boolean;
};
rating: NextcloudRating;
/**
@@ -125,6 +132,7 @@ export class NextcloudStatusService {
private toView(row: PublicRow, reference: NextcloudReference | null): NextcloudInstanceView {
const status = {
pendingRetry: row.consecutiveFailures === 1,
checkedAt: row.lastCheckedAt ? row.lastCheckedAt.toISOString() : null,
reachable: row.reachable,
maintenance: row.maintenance,
@@ -271,6 +279,22 @@ export class NextcloudStatusService {
if (baseUrl !== existing.baseUrl) {
data.baseUrl = baseUrl;
urlChanged = true;
// Neue Adresse: der alte Pruefstand gilt nicht mehr (D-K8). `alertState`
// bleibt bewusst unberuehrt — wird eine kaputte Adresse korrigiert und
// antwortet die neue, geht "wieder in Ordnung" an die Abonnenten.
Object.assign(data, {
reachable: null,
maintenance: null,
needsDbUpgrade: null,
versionString: null,
edition: null,
productName: null,
errorKind: null,
errorDetail: null,
lastCheckedAt: null,
consecutiveFailures: 0,
firstFailureAt: null,
});
}
}
@@ -403,11 +427,25 @@ export class NextcloudStatusService {
* Leserecht ueber `system_read_policy ... FOR SELECT` der Migration
* 20261002150000): nur Kennung und Mandant ALLER Clouds, nie Logo-Bytes oder
* Adressen. Geprueft und geschrieben wird danach je Cloud an ihren eigenen
* Mandanten gebunden (`checkInstance`).
* Mandanten gebunden (`checkInstance`). Mit `retryDueBefore` (Wiederholung
* nach dem ersten Fehlschlag, quick-261002-kxc) filtert dieselbe Abfrage auf
* Clouds mit genau einem Fehlschlag, der vor diesem Zeitpunkt lag.
*/
async loadAllInstancesForScheduler(): Promise<{ id: string; tenantId: string }[]> {
async loadAllInstancesForScheduler(filter?: {
retryDueBefore: Date;
}): Promise<{ id: string; tenantId: string }[]> {
const systemPrisma = forSystem(this.prisma);
return systemPrisma.nextcloudInstance.findMany({
// Wiederholungsauftrag (D-K3): nur Clouds mit genau einem Fehlschlag, dessen
// Zeitpunkt lange genug zurueckliegt — derselbe Systemlesezugriff, kein neuer.
...(filter
? {
where: {
consecutiveFailures: 1,
firstFailureAt: { lte: filter.retryDueBefore },
},
}
: {}),
select: { id: true, tenantId: true },
});
}