Added Debug Log and info pickup older 6month + hygiene crt
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const axios = require('axios');
|
||||
const requestLogStore = require('./requestLogStore');
|
||||
|
||||
const BASE_URL = 'https://foodsharing.de';
|
||||
|
||||
@@ -11,6 +12,49 @@ const client = axios.create({
|
||||
}
|
||||
});
|
||||
|
||||
client.interceptors.request.use((config) => {
|
||||
config.metadata = { startedAt: Date.now() };
|
||||
return config;
|
||||
});
|
||||
|
||||
client.interceptors.response.use(
|
||||
(response) => {
|
||||
const startedAt = response?.config?.metadata?.startedAt || Date.now();
|
||||
try {
|
||||
requestLogStore.add({
|
||||
direction: 'outgoing',
|
||||
target: 'foodsharing.de',
|
||||
method: (response.config?.method || 'GET').toUpperCase(),
|
||||
path: response.config?.url || '',
|
||||
status: response.status,
|
||||
durationMs: Date.now() - startedAt,
|
||||
responseBody: response.data
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[REQUEST-LOG] Outgoing-Log fehlgeschlagen:', error.message);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
const startedAt = error?.config?.metadata?.startedAt || Date.now();
|
||||
try {
|
||||
requestLogStore.add({
|
||||
direction: 'outgoing',
|
||||
target: 'foodsharing.de',
|
||||
method: (error.config?.method || 'GET').toUpperCase(),
|
||||
path: error.config?.url || '',
|
||||
status: error?.response?.status || null,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error: error?.message || 'Unbekannter Fehler',
|
||||
responseBody: error?.response?.data
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[REQUEST-LOG] Outgoing-Error-Log fehlgeschlagen:', logError.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
function extractCsrfToken(cookies = []) {
|
||||
if (!Array.isArray(cookies)) {
|
||||
return null;
|
||||
@@ -230,6 +274,16 @@ async function pickupRuleCheck(storeId, utcDate, profileId, session) {
|
||||
return response.data?.result === true;
|
||||
}
|
||||
|
||||
async function fetchStoreMembers(storeId, cookieHeader) {
|
||||
if (!storeId) {
|
||||
return [];
|
||||
}
|
||||
const response = await client.get(`/api/stores/${storeId}/member`, {
|
||||
headers: buildHeaders(cookieHeader)
|
||||
});
|
||||
return Array.isArray(response.data) ? response.data : [];
|
||||
}
|
||||
|
||||
async function bookSlot(storeId, utcDate, profileId, session) {
|
||||
await client.post(
|
||||
`/api/stores/${storeId}/pickups/${utcDate}/${profileId}`,
|
||||
@@ -248,6 +302,7 @@ module.exports = {
|
||||
fetchPickups,
|
||||
fetchRegionStores,
|
||||
fetchStoreDetails,
|
||||
fetchStoreMembers,
|
||||
pickupRuleCheck,
|
||||
bookSlot
|
||||
};
|
||||
|
||||
@@ -199,10 +199,29 @@ async function sendTestNotification(profileId, channel) {
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
async function sendDormantPickupWarning({ profileId, storeName, storeId, reasonLines = [] }) {
|
||||
if (!profileId || !Array.isArray(reasonLines) || reasonLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const adminSettings = adminConfig.readSettings();
|
||||
const userSettings = readNotificationSettings(profileId);
|
||||
const storeLink = storeId ? `https://foodsharing.de/store/${storeId}` : null;
|
||||
const title = `Prüfung fällig: ${storeName}`;
|
||||
const messageBody = reasonLines.join('\n');
|
||||
const message = storeLink ? `${messageBody}\n${storeLink}` : messageBody;
|
||||
|
||||
await sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, {
|
||||
title,
|
||||
message,
|
||||
priority: 'high'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendSlotNotification,
|
||||
sendStoreWatchNotification,
|
||||
sendStoreWatchSummaryNotification,
|
||||
sendTestNotification,
|
||||
sendDesiredWindowMissedNotification
|
||||
sendDesiredWindowMissedNotification,
|
||||
sendDormantPickupWarning
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const notificationService = require('./notificationService');
|
||||
const { readConfig, writeConfig } = require('./configStore');
|
||||
const { readStoreWatch, writeStoreWatch } = require('./storeWatchStore');
|
||||
const { setStoreStatus, persistStoreStatusCache } = require('./storeStatusCache');
|
||||
const { sendDormantPickupWarning } = require('./notificationService');
|
||||
|
||||
function wait(ms) {
|
||||
if (!ms || ms <= 0) {
|
||||
@@ -557,6 +558,7 @@ function scheduleEntry(sessionId, entry, settings) {
|
||||
function scheduleConfig(sessionId, config, settings) {
|
||||
const resolvedSettings = resolveSettings(settings);
|
||||
sessionStore.clearJobs(sessionId);
|
||||
scheduleDormantMembershipCheck(sessionId);
|
||||
const watchScheduled = scheduleStoreWatchers(sessionId, resolvedSettings);
|
||||
const entries = Array.isArray(config) ? config : [];
|
||||
const activeEntries = entries.filter((entry) => entry.active);
|
||||
@@ -581,6 +583,100 @@ async function runStoreWatchCheck(sessionId, settings, options = {}) {
|
||||
return checkWatchedStores(sessionId, resolvedSettings, options);
|
||||
}
|
||||
|
||||
function setMonthOffset(date, offset) {
|
||||
const copy = new Date(date.getTime());
|
||||
copy.setMonth(copy.getMonth() + offset);
|
||||
return copy;
|
||||
}
|
||||
|
||||
async function checkDormantMembers(sessionId) {
|
||||
const session = sessionStore.get(sessionId);
|
||||
if (!session?.profile?.id) {
|
||||
return;
|
||||
}
|
||||
const profileId = session.profile.id;
|
||||
const ensured = await ensureSession(session);
|
||||
if (!ensured) {
|
||||
return;
|
||||
}
|
||||
const config = readConfig(profileId);
|
||||
const skipMap = new Map();
|
||||
config.forEach((entry) => {
|
||||
if (entry?.id) {
|
||||
skipMap.set(String(entry.id), !!entry.skipDormantCheck);
|
||||
}
|
||||
});
|
||||
|
||||
const stores = Array.isArray(session.storesCache?.data) ? session.storesCache.data : [];
|
||||
if (stores.length === 0) {
|
||||
console.warn(`[DORMANT] Keine Stores für Session ${sessionId} im Cache gefunden.`);
|
||||
}
|
||||
const fourMonthsAgo = setMonthOffset(new Date(), -4).getTime();
|
||||
const hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id ? String(store.id) : null;
|
||||
if (!storeId) {
|
||||
continue;
|
||||
}
|
||||
if (skipMap.get(storeId)) {
|
||||
continue;
|
||||
}
|
||||
let members = [];
|
||||
try {
|
||||
members = await foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader);
|
||||
} catch (error) {
|
||||
console.warn(`[DORMANT] Mitglieder von Store ${storeId} konnten nicht geladen werden:`, error.message);
|
||||
continue;
|
||||
}
|
||||
const memberEntry = members.find((m) => String(m?.id) === String(profileId));
|
||||
if (!memberEntry) {
|
||||
continue;
|
||||
}
|
||||
const reasons = [];
|
||||
const lastFetchMs = memberEntry.last_fetch ? Number(memberEntry.last_fetch) * 1000 : null;
|
||||
if (!lastFetchMs || lastFetchMs < fourMonthsAgo) {
|
||||
const lastFetchLabel = lastFetchMs ? new Date(lastFetchMs).toLocaleDateString('de-DE') : 'unbekannt';
|
||||
reasons.push(`Letzte Abholung: ${lastFetchLabel} (älter als 4 Monate)`);
|
||||
}
|
||||
if (memberEntry.hygiene_certificate_until) {
|
||||
const expiry = new Date(memberEntry.hygiene_certificate_until.replace(' ', 'T'));
|
||||
if (!Number.isNaN(expiry.getTime()) && expiry.getTime() < hygieneCutoff) {
|
||||
reasons.push(
|
||||
`Hygiene-Nachweis läuft bald ab: ${expiry.toLocaleDateString('de-DE')} (unter 6 Wochen)`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (reasons.length > 0) {
|
||||
try {
|
||||
await sendDormantPickupWarning({
|
||||
profileId,
|
||||
storeName: store.name || `Store ${storeId}`,
|
||||
storeId,
|
||||
reasonLines: reasons
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[DORMANT] Warnung für Store ${storeId} konnte nicht versendet werden:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleDormantMembershipCheck(sessionId) {
|
||||
const cronExpression = '0 4 */14 * *';
|
||||
const job = cron.schedule(
|
||||
cronExpression,
|
||||
() => {
|
||||
checkDormantMembers(sessionId).catch((error) => {
|
||||
console.error('[DORMANT] Prüfung fehlgeschlagen:', error.message);
|
||||
});
|
||||
},
|
||||
{ timezone: 'Europe/Berlin' }
|
||||
);
|
||||
sessionStore.attachJob(sessionId, job);
|
||||
setTimeout(() => checkDormantMembers(sessionId), randomDelayMs(30, 180));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scheduleConfig,
|
||||
runStoreWatchCheck
|
||||
|
||||
93
services/requestLogStore.js
Normal file
93
services/requestLogStore.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||
const LOG_FILE = path.join(CONFIG_DIR, 'request-logs.json');
|
||||
const TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
||||
const MAX_BODY_CHARS = 10000;
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readLogs() {
|
||||
try {
|
||||
ensureDir();
|
||||
if (!fs.existsSync(LOG_FILE)) {
|
||||
return [];
|
||||
}
|
||||
const raw = fs.readFileSync(LOG_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
console.warn('[REQUEST-LOG] Konnte Logdatei nicht lesen:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function persistLogs(logs) {
|
||||
try {
|
||||
ensureDir();
|
||||
fs.writeFileSync(LOG_FILE, JSON.stringify(logs, null, 2));
|
||||
} catch (error) {
|
||||
console.warn('[REQUEST-LOG] Konnte Logdatei nicht schreiben:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function prune(logs = []) {
|
||||
const cutoff = Date.now() - TTL_MS;
|
||||
return logs.filter((entry) => Number(entry?.timestamp) >= cutoff);
|
||||
}
|
||||
|
||||
function serializeBodySnippet(body) {
|
||||
try {
|
||||
if (body === undefined || body === null) {
|
||||
return null;
|
||||
}
|
||||
let text = '';
|
||||
if (typeof body === 'string') {
|
||||
text = body;
|
||||
} else if (Buffer.isBuffer(body)) {
|
||||
text = body.toString('utf8');
|
||||
} else {
|
||||
text = JSON.stringify(body);
|
||||
}
|
||||
if (text.length > MAX_BODY_CHARS) {
|
||||
return `${text.slice(0, MAX_BODY_CHARS)}… (gekürzt)`;
|
||||
}
|
||||
return text;
|
||||
} catch (error) {
|
||||
return `<<Konnte Response nicht serialisieren: ${error.message}>>`;
|
||||
}
|
||||
}
|
||||
|
||||
function add(entry = {}) {
|
||||
const logs = prune(readLogs());
|
||||
const record = {
|
||||
id: uuid(),
|
||||
timestamp: Date.now(),
|
||||
...entry
|
||||
};
|
||||
if ('responseBody' in record) {
|
||||
record.responseBody = serializeBodySnippet(record.responseBody);
|
||||
}
|
||||
logs.push(record);
|
||||
persistLogs(logs);
|
||||
return record;
|
||||
}
|
||||
|
||||
function list(limit = 500) {
|
||||
const sanitizedLimit = Math.max(1, Math.min(Number(limit) || 500, 2000));
|
||||
const logs = prune(readLogs());
|
||||
persistLogs(logs);
|
||||
return logs.slice(-sanitizedLimit).reverse();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
add,
|
||||
list,
|
||||
serializeBodySnippet
|
||||
};
|
||||
Reference in New Issue
Block a user