feat(nextcloud-status): Clouds verwalten, Logos, stündliche Prüfung und Sortierung
- Ändern, Entfernen, Logo-Upload und -Abruf, Sammelprüfung nur mit Verwalten - Stündlicher Auftrag beim Start registriert, Prüfung je Cloud an ihren Mandanten gebunden - Sortierung nach Kundenname, Status, Version und Support-Ende, je Benutzer gemerkt - Formular zum Anlegen und Bearbeiten, Zugriffsinventar angepasst Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { NextcloudReleaseService } from './nextcloud-release.service';
|
||||
import {
|
||||
CHECK_CONCURRENCY,
|
||||
NextcloudStatusService,
|
||||
runWithConcurrency,
|
||||
} from './nextcloud-status.service';
|
||||
|
||||
/**
|
||||
* CronJob constructor — resolved at runtime via require() because `cron` is
|
||||
* a transitive dependency of @nestjs/schedule (not a direct api dep under
|
||||
* pnpm strict isolation, so `import { CronJob } from 'cron'` fails
|
||||
* type-check). At runtime, cron IS on disk as @nestjs/schedule@6 declares
|
||||
* it as a peer dep. Reuses the exact ProxmoxSchedulerService resolution
|
||||
* workaround verbatim.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const CronJobClass: new (cronTime: string, onTick: () => void) => { start(): void } =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
require('cron').CronJob as new (
|
||||
cronTime: string,
|
||||
onTick: () => void,
|
||||
) => { start(): void };
|
||||
|
||||
/** Name des Auftrags in der Registry. */
|
||||
export const NEXTCLOUD_JOB_NAME = 'nextcloud-status-poll';
|
||||
/** Jede volle Stunde (L-08). */
|
||||
export const NEXTCLOUD_CRON = '0 * * * *';
|
||||
|
||||
/**
|
||||
* NextcloudStatusSchedulerService — stuendliche Pruefung aller Clouds
|
||||
* (quick-261002-k67, L-08, D-C).
|
||||
*
|
||||
* Lebenszyklus: `OnApplicationBootstrap`, NICHT `OnModuleInit` — die
|
||||
* Reihenfolge der `onModuleInit`-Haken zwischen Modulen ist nicht
|
||||
* festgelegt, und die Erfahrung "frische Datenbank ingestiert nichts bis zum
|
||||
* zweiten Neustart" (Tender-Cron-Bootstrap) gilt hier genauso.
|
||||
*
|
||||
* Ein einziger globaler Auftrag statt je einem je Mandant: das Intervall ist
|
||||
* fest (stuendlich), es gibt keine Einstellung je Zeile. Der Auftrag wird
|
||||
* beim Start OHNE Datenbankzugriff registriert — eine frische Datenbank kann
|
||||
* daher nie ohne Auftrag enden, auch wenn beim Start noch keine Cloud
|
||||
* eingetragen ist. Jeder Durchlauf liest Kennung und Mandant aller Clouds
|
||||
* (ein einziger Systemkontext-Aufruf) und prueft dann jede Cloud an ihren
|
||||
* eigenen Mandanten gebunden, mit hoechstens vier gleichzeitig. Ein
|
||||
* Ueberlappungsschutz ueberspringt einen Durchlauf, solange der vorige laeuft.
|
||||
*/
|
||||
@Injectable()
|
||||
export class NextcloudStatusSchedulerService implements OnApplicationBootstrap {
|
||||
private readonly logger = new Logger(NextcloudStatusSchedulerService.name);
|
||||
private running = false;
|
||||
|
||||
constructor(
|
||||
private readonly schedulerRegistry: SchedulerRegistry,
|
||||
private readonly service: NextcloudStatusService,
|
||||
private readonly release: NextcloudReleaseService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registriert und startet den Auftrag und stoesst das Aufwaermen der
|
||||
* Vergleichsdaten an (ohne zu warten). Ein Fehler wird gefangen und
|
||||
* protokolliert, nie weitergeworfen — die Anwendung startet trotzdem.
|
||||
*/
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
try {
|
||||
const job = new CronJobClass(NEXTCLOUD_CRON, () => {
|
||||
this.tick().catch((err) =>
|
||||
this.logger.error(`Nextcloud poll tick failed: ${(err as Error).message}`),
|
||||
);
|
||||
});
|
||||
// Cast noetig — dasselbe Muster wie ProxmoxSchedulerService.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Cast wie in ProxmoxSchedulerService
|
||||
this.schedulerRegistry.addCronJob(NEXTCLOUD_JOB_NAME, job as any);
|
||||
job.start();
|
||||
this.logger.log(`Nextcloud-Status cron job registered: ${NEXTCLOUD_CRON}`);
|
||||
void this.release.refresh().catch(() => undefined);
|
||||
} catch (err) {
|
||||
this.logger.error(`Nextcloud-Status scheduler init failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ein Durchlauf ueber alle Clouds aller Mandanten. */
|
||||
async tick(): Promise<void> {
|
||||
if (this.running) {
|
||||
this.logger.warn('Nextcloud poll tick skipped — previous run still active');
|
||||
return;
|
||||
}
|
||||
this.running = true;
|
||||
try {
|
||||
const rows = await this.service.loadAllInstancesForScheduler();
|
||||
// Je Mandant gruppiert, damit jede Pruefung an IHREN Mandanten gebunden bleibt.
|
||||
const byTenant = new Map<string, string[]>();
|
||||
for (const row of rows) {
|
||||
const ids = byTenant.get(row.tenantId) ?? [];
|
||||
ids.push(row.id);
|
||||
byTenant.set(row.tenantId, ids);
|
||||
}
|
||||
const work: { tenantId: string; id: string }[] = [];
|
||||
for (const [tenantId, ids] of byTenant) {
|
||||
for (const id of ids) work.push({ tenantId, id });
|
||||
}
|
||||
await runWithConcurrency(work, CHECK_CONCURRENCY, async ({ tenantId, id }) => {
|
||||
try {
|
||||
await this.service.checkInstance(tenantId, id);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Nextcloud check failed for instance ${id} (tenant ${tenantId}): ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user