feat: refresh stale last pickup information

Track the successful member-list check time and refresh stale pickup information
asynchronously after the GUI loads without triggering dormant warnings.
This commit is contained in:
2026-08-05 20:00:16 +02:00
parent d17bb63e23
commit 713f52caa0
6 changed files with 192 additions and 29 deletions

View File

@@ -0,0 +1,63 @@
const LAST_PICKUP_INFO_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
function parseTimestamp(value) {
if (!value) {
return null;
}
const timestamp = value instanceof Date ? value.getTime() : new Date(value).getTime();
return Number.isFinite(timestamp) ? timestamp : null;
}
function getStaleLastPickupStoreIds(config = [], now = Date.now()) {
if (!Array.isArray(config)) {
return [];
}
return config
.filter((entry) => {
if (!entry?.id || entry.hidden) {
return false;
}
const checkedAt = parseTimestamp(entry.lastPickupCheckedAt);
return !checkedAt || now - checkedAt > LAST_PICKUP_INFO_MAX_AGE_MS;
})
.map((entry) => String(entry.id));
}
function parseLastFetchTimestamp(value) {
if (typeof value === 'string') {
return parseTimestamp(value);
}
if (!Number.isFinite(Number(value))) {
return null;
}
const numericValue = Number(value);
return numericValue > 1e12 ? numericValue : numericValue * 1000;
}
function updateLastPickupInfo(entry, lastFetchValue, checkedAt = new Date()) {
if (!entry || typeof entry !== 'object') {
return false;
}
let changed = false;
const checkedAtIso = new Date(checkedAt).toISOString();
if (entry.lastPickupCheckedAt !== checkedAtIso) {
entry.lastPickupCheckedAt = checkedAtIso;
changed = true;
}
const lastFetchTimestamp = parseLastFetchTimestamp(lastFetchValue);
if (lastFetchTimestamp !== null) {
const lastPickupAt = new Date(lastFetchTimestamp).toISOString();
if (entry.lastPickupAt !== lastPickupAt) {
entry.lastPickupAt = lastPickupAt;
changed = true;
}
}
return changed;
}
module.exports = {
LAST_PICKUP_INFO_MAX_AGE_MS,
getStaleLastPickupStoreIds,
parseLastFetchTimestamp,
updateLastPickupInfo
};

View File

@@ -15,6 +15,7 @@ const maintenanceMode = require('./maintenanceMode');
const { computeReminderSchedule } = require('./journalReminderUtils');
const { startStoreMessageWatcher } = require('./storeMessageWatcher');
const { getBlockingProfilePickup } = require('./profileCheckTolerance');
const { getStaleLastPickupStoreIds, parseLastFetchTimestamp, updateLastPickupInfo } = require('./lastPickupInfo');
function wait(ms) {
if (!ms || ms <= 0) {
@@ -53,6 +54,7 @@ let regularPickupActive = 0;
let regularPickupRefreshJob = null;
const dormantWarningCooldowns = new Map();
const DORMANT_WARNING_COOLDOWN_MS = 6 * 60 * 60 * 1000;
const lastPickupRefreshInFlight = new Map();
function ensureRegularPickupCacheDir() {
const dir = path.dirname(REGULAR_PICKUP_CACHE_FILE);
@@ -1385,19 +1387,21 @@ function getMissingLastPickupStoreIds(config = []) {
async function checkDormantMembers(sessionId, options = {}) {
if (shouldPauseForMaintenance(`Dormant-Check für Session ${sessionId}`)) {
return;
return { checked: 0, updated: false };
}
const session = sessionStore.get(sessionId);
if (!session?.profile?.id) {
return;
return { checked: 0, updated: false };
}
const storeIdSet = Array.isArray(options.storeIds)
? new Set(options.storeIds.map((storeId) => String(storeId)))
: null;
const includeSkippedDormant = !!options.includeSkippedDormant;
const sendWarnings = options.sendWarnings !== false;
const profileId = session.profile.id;
const ensured = await ensureSession(session);
if (!ensured) {
return;
return { checked: 0, updated: false };
}
const config = readConfig(profileId);
const skipMap = new Map();
@@ -1420,7 +1424,7 @@ async function checkDormantMembers(sessionId, options = {}) {
if (storeIdSet && !storeIdSet.has(storeId)) {
return;
}
if (skipMap.get(storeId)) {
if (!includeSkippedDormant && skipMap.get(storeId)) {
return;
}
storeTargets.set(storeId, {
@@ -1432,7 +1436,7 @@ async function checkDormantMembers(sessionId, options = {}) {
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.`);
return;
return { checked: 0, updated: false };
}
const activeStoreIds = new Set();
@@ -1476,11 +1480,12 @@ async function checkDormantMembers(sessionId, options = {}) {
});
if (storeTargets.size === 0) {
return;
return { checked: 0, updated: false };
}
const fourMonthsAgo = setMonthOffset(new Date(), -4).getTime();
const hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000;
let checked = 0;
for (const target of storeTargets.values()) {
const storeId = target.storeId;
let members = [];
@@ -1505,33 +1510,21 @@ async function checkDormantMembers(sessionId, options = {}) {
if (!memberEntry) {
continue;
}
checked += 1;
const reasons = [];
const lastFetchRaw = memberEntry.lastFetch ?? memberEntry.last_fetch ?? null;
let lastFetchMs = null;
if (typeof lastFetchRaw === 'string') {
const parsedLastFetch = new Date(lastFetchRaw);
if (!Number.isNaN(parsedLastFetch.getTime())) {
lastFetchMs = parsedLastFetch.getTime();
}
} else if (Number.isFinite(Number(lastFetchRaw))) {
const numericLastFetch = Number(lastFetchRaw);
lastFetchMs = numericLastFetch > 1e12 ? numericLastFetch : numericLastFetch * 1000;
const lastFetchMs = parseLastFetchTimestamp(lastFetchRaw);
const configEntry = configEntryMap.get(storeId)?.entry;
if (updateLastPickupInfo(configEntry, lastFetchRaw)) {
configChanged = true;
}
if (Number.isFinite(lastFetchMs)) {
const configEntry = configEntryMap.get(storeId)?.entry;
const lastPickupAt = new Date(lastFetchMs).toISOString();
if (configEntry && configEntry.lastPickupAt !== lastPickupAt) {
configEntry.lastPickupAt = lastPickupAt;
configChanged = true;
}
}
if (!lastFetchMs || lastFetchMs < fourMonthsAgo) {
if (sendWarnings && !skipMap.get(storeId) && (!lastFetchMs || lastFetchMs < fourMonthsAgo)) {
const lastFetchLabel = lastFetchMs ? new Date(lastFetchMs).toLocaleDateString('de-DE') : 'unbekannt';
reasons.push(`Letzte Abholung: ${lastFetchLabel} (älter als 4 Monate)`);
}
const hygieneCertificateUntil =
memberEntry.hygieneCertificateUntil ?? memberEntry.hygiene_certificate_until ?? null;
if (hygieneCertificateUntil) {
if (sendWarnings && !skipMap.get(storeId) && hygieneCertificateUntil) {
const expiry = new Date(String(hygieneCertificateUntil).replace(' ', 'T'));
if (!Number.isNaN(expiry.getTime()) && expiry.getTime() < hygieneCutoff) {
reasons.push(
@@ -1539,7 +1532,7 @@ async function checkDormantMembers(sessionId, options = {}) {
);
}
}
if (reasons.length > 0) {
if (sendWarnings && !skipMap.get(storeId) && reasons.length > 0) {
const cooldownKey = `${profileId}:${storeId}`;
const lastSentAt = dormantWarningCooldowns.get(cooldownKey);
if (lastSentAt && Date.now() - lastSentAt < DORMANT_WARNING_COOLDOWN_MS) {
@@ -1566,6 +1559,37 @@ async function checkDormantMembers(sessionId, options = {}) {
console.error(`[DORMANT] Letzte Abholung für Profil ${profileId} konnte nicht gespeichert werden:`, error.message);
}
}
return { checked, updated: configChanged };
}
async function refreshStaleLastPickupInfo(sessionId) {
const session = sessionStore.get(sessionId);
const profileId = session?.profile?.id ? String(session.profile.id) : null;
if (!profileId) {
return { checked: 0, updated: false, stale: 0 };
}
if (lastPickupRefreshInFlight.has(profileId)) {
return lastPickupRefreshInFlight.get(profileId);
}
const refresh = (async () => {
const config = readConfig(profileId);
const staleStoreIds = getStaleLastPickupStoreIds(config);
if (staleStoreIds.length === 0) {
return { checked: 0, updated: false, stale: 0 };
}
const result = await checkDormantMembers(sessionId, {
storeIds: staleStoreIds,
includeSkippedDormant: true,
sendWarnings: false
});
return { ...result, stale: staleStoreIds.length };
})();
lastPickupRefreshInFlight.set(profileId, refresh);
try {
return await refresh;
} finally {
lastPickupRefreshInFlight.delete(profileId);
}
}
function scheduleDormantMembershipCheck(sessionId, settings) {
@@ -1680,6 +1704,7 @@ module.exports = {
runStoreWatchCheck,
runImmediatePickupCheck,
runDormantMembershipCheck,
refreshStaleLastPickupInfo,
getRegularPickupSchedule,
scheduleRegularPickupRefresh
};