This commit is contained in:
Matthias Hochmeister
2026-03-13 15:04:40 +01:00
parent 8d9388ca9a
commit 7833dca29c
3 changed files with 25 additions and 7 deletions

View File

@@ -17,7 +17,9 @@ import nextcloudService from '../services/nextcloud.service';
import logger from '../utils/logger'; import logger from '../utils/logger';
const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
const NEXTCLOUD_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes const NEXTCLOUD_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes — keep reasonable to avoid rate-limiting
const NEXTCLOUD_BATCH_DELAY_MS = 300; // ms between user batches to avoid hammering Nextcloud
const STARTUP_DELAY_MS = 30 * 1000; // 30 seconds — avoid burst on container restart
const ATEMSCHUTZ_THRESHOLD = 60; // days const ATEMSCHUTZ_THRESHOLD = 60; // days
let jobInterval: ReturnType<typeof setInterval> | null = null; let jobInterval: ReturnType<typeof setInterval> | null = null;
@@ -267,6 +269,9 @@ async function generateNextcloudTalkNotifications(): Promise<void> {
const users = usersResult.rows; const users = usersResult.rows;
for (let i = 0; i < users.length; i += NEXTCLOUD_BATCH_SIZE) { for (let i = 0; i < users.length; i += NEXTCLOUD_BATCH_SIZE) {
if (i > 0) {
await new Promise((r) => setTimeout(r, NEXTCLOUD_BATCH_DELAY_MS));
}
const batch = users.slice(i, i + NEXTCLOUD_BATCH_SIZE); const batch = users.slice(i, i + NEXTCLOUD_BATCH_SIZE);
await Promise.allSettled(batch.map((user) => processNextcloudUser(user))); await Promise.allSettled(batch.map((user) => processNextcloudUser(user)));
} }
@@ -318,9 +323,11 @@ export function startNotificationJob(): void {
return; return;
} }
// Run both once on startup, then repeat on separate intervals. // Run main job once on startup, then repeat.
runNotificationGeneration(); runNotificationGeneration();
runNextcloudNotificationGeneration();
// Delay initial Nextcloud run to avoid a burst on container restart.
setTimeout(() => runNextcloudNotificationGeneration(), STARTUP_DELAY_MS);
jobInterval = setInterval(() => { jobInterval = setInterval(() => {
runNotificationGeneration(); runNotificationGeneration();

View File

@@ -86,8 +86,15 @@ function buildHeaders(): Record<string, string> {
* Fetches all BookStack books and returns a map of book_id → book_slug. * Fetches all BookStack books and returns a map of book_id → book_slug.
* The /api/pages list endpoint does not reliably include book_slug, so we * The /api/pages list endpoint does not reliably include book_slug, so we
* look it up separately and use it when constructing page URLs. * look it up separately and use it when constructing page URLs.
* Cached for 5 minutes to avoid hammering the API on every dashboard load.
*/ */
let bookSlugMapCache: { map: Map<number, string>; expiresAt: number } | null = null;
const BOOK_SLUG_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
async function getBookSlugMap(): Promise<Map<number, string>> { async function getBookSlugMap(): Promise<Map<number, string>> {
if (bookSlugMapCache && Date.now() < bookSlugMapCache.expiresAt) {
return bookSlugMapCache.map;
}
const { bookstack } = environment; const { bookstack } = environment;
try { try {
const response = await httpClient.get( const response = await httpClient.get(
@@ -95,9 +102,11 @@ async function getBookSlugMap(): Promise<Map<number, string>> {
{ params: { count: 500 }, headers: buildHeaders() }, { params: { count: 500 }, headers: buildHeaders() },
); );
const books: Array<{ id: number; slug: string }> = response.data?.data ?? []; const books: Array<{ id: number; slug: string }> = response.data?.data ?? [];
return new Map(books.map((b) => [b.id, b.slug])); const map = new Map(books.map((b) => [b.id, b.slug]));
bookSlugMapCache = { map, expiresAt: Date.now() + BOOK_SLUG_CACHE_TTL_MS };
return map;
} catch { } catch {
return new Map(); return bookSlugMapCache?.map ?? new Map();
} }
} }

View File

@@ -435,7 +435,7 @@ function MitgliedDetail() {
</Box> </Box>
{/* Edit controls */} {/* Edit controls */}
{canEdit && ( {canEdit && (canWrite || !!profile) && (
<Box> <Box>
{editMode ? ( {editMode ? (
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
@@ -475,7 +475,9 @@ function MitgliedDetail() {
{!profile && ( {!profile && (
<Alert severity="info" sx={{ mt: 2 }}> <Alert severity="info" sx={{ mt: 2 }}>
Für dieses Mitglied wurde noch kein Profil angelegt. Für dieses Mitglied wurde noch kein Profil angelegt.
{canWrite && ' Ein Kommandant kann das Profil unter "Stammdaten" erstellen.'} {canWrite
? ' Ein Kommandant kann das Profil unter "Stammdaten" erstellen.'
: ' Wende dich an einen Administrator, um dein Profil anlegen zu lassen.'}
</Alert> </Alert>
)} )}