feat: KöPi Tages-Cron um 06:00 Uhr mit Pushover-Benachrichtigung bei Angebotsänderung

This commit is contained in:
2026-07-10 13:26:33 +02:00
parent 940bf3f103
commit 1900d73490
2 changed files with 85 additions and 3 deletions

View File

@@ -256,6 +256,7 @@ server.listen(4000, () => console.log('🚀 Dicken Dock läuft auf Port 4000'));
// ── Push-Zeitplaner Hintergrund-Job ────────────────────────────────────────── // ── Push-Zeitplaner Hintergrund-Job ──────────────────────────────────────────
const db = require('./db'); const db = require('./db');
const koepiRoutes = require('./tools/koepi/routes');
async function sendPushoverMsg(userKey, appToken, message, opts = {}) { async function sendPushoverMsg(userKey, appToken, message, opts = {}) {
try { try {
@@ -296,6 +297,20 @@ async function runScheduler() {
console.log(`✓ Push gesendet an user ${row.user_id}: "${row.message}" (fällig: ${row.scheduled_at})`); console.log(`✓ Push gesendet an user ${row.user_id}: "${row.message}" (fällig: ${row.scheduled_at})`);
} }
} catch(e) { console.error('Push-Job Fehler:', e.message); } } catch(e) { console.error('Push-Job Fehler:', e.message); }
// KöPi Tages-Check: täglich um 06:00 (localtime), Duplikatschutz über admin_settings-Datum
try {
const now = db.prepare("SELECT datetime('now','localtime') as t").get().t;
const date = now.slice(0, 10);
const time = now.slice(11, 16);
if (time === '06:00') {
const lastRunDate = db.prepare("SELECT value FROM admin_settings WHERE key='koepi_cron_last_date'").get()?.value;
if (lastRunDate !== date) {
db.prepare("INSERT OR REPLACE INTO admin_settings (key, value) VALUES ('koepi_cron_last_date', ?)").run(date);
await koepiRoutes.runDailyCheck();
}
}
} catch(e) { console.error('KöPi-Cron Fehler:', e.message); }
} }
console.log(`⏰ Push-Scheduler aktiv nächste Prüfung in ${Math.round(msToNextMinute/1000)}s`); console.log(`⏰ Push-Scheduler aktiv nächste Prüfung in ${Math.round(msToNextMinute/1000)}s`);

View File

@@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const https = require('https'); const https = require('https');
const db = require('../../db');
const { authenticate } = require('../../middleware/auth'); const { authenticate } = require('../../middleware/auth');
const router = express.Router(); const router = express.Router();
@@ -40,10 +41,12 @@ function getAddress(retailer) {
} }
// ── Angebote via Puppeteer von marktguru ───────────────────────────────────── // ── Angebote via Puppeteer von marktguru ─────────────────────────────────────
async function scrapeMarktguru() { async function scrapeMarktguru(forceRefresh = false) {
const cacheKey = 'koepi:mg'; const cacheKey = 'koepi:mg';
const cached = cacheGet(cacheKey); if (!forceRefresh) {
if (cached) return cached; const cached = cacheGet(cacheKey);
if (cached) return cached;
}
let puppeteer; let puppeteer;
try { puppeteer = require('puppeteer-core'); } try { puppeteer = require('puppeteer-core'); }
@@ -256,6 +259,69 @@ router.get('/img', authenticate, (req, res) => {
} catch { res.status(400).end(); } } catch { res.status(400).end(); }
}); });
// ── Tages-Cron: neue/geänderte Angebote per Pushover melden ─────────────────
const KOEPI_SETTINGS_KEY = 'koepi_last_offers';
// Vergleichbare, stabile Signatur eines Angebots (Reihenfolge-unabhängig)
function offerSignature(offers) {
return offers
.map(o => `${(o.retailer || '').toLowerCase().trim()}|${(o.price || '').trim()}|${(o.validity || o.dateRange || '').trim()}`)
.sort()
.join('\n');
}
function formatOfferMessage(offers) {
if (!offers.length) return 'Aktuell keine König Pilsener Angebote gefunden.';
return offers
.map(o => `${o.retailer || '?'}: ${o.price || '?'} (${o.validity || o.dateRange || 'Gültigkeit unbekannt'})`)
.join('\n');
}
async function sendKoepiPushover(offers) {
const recipients = db.prepare(`
SELECT user_key, app_token, retry, expire FROM pushover_settings
WHERE user_key IS NOT NULL AND app_token IS NOT NULL
`).all();
const message = formatOfferMessage(offers);
for (const r of recipients) {
try {
const params = {
token: r.app_token,
user: r.user_key,
title: '🍺 KöPi Angebote',
message,
};
if (r.retry && r.expire) { params.priority = 2; params.retry = r.retry; params.expire = r.expire; }
await fetch('https://api.pushover.net/1/messages.json', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params),
});
} catch (e) { console.error('KöPi Pushover Fehler:', e.message); }
}
}
// Wird täglich um 06:00 vom Scheduler in index.js aufgerufen
async function runDailyCheck() {
try {
const { offers } = await scrapeMarktguru(true);
const currentSig = offerSignature(offers);
const lastSig = db.prepare('SELECT value FROM admin_settings WHERE key=?').get(KOEPI_SETTINGS_KEY)?.value || '';
if (currentSig === lastSig) {
console.log('🍺 KöPi-Check: keine Änderungen an den Angeboten.');
return;
}
await sendKoepiPushover(offers);
db.prepare('INSERT OR REPLACE INTO admin_settings (key, value) VALUES (?, ?)')
.run(KOEPI_SETTINGS_KEY, currentSig);
console.log(`🍺 KöPi-Check: ${offers.length} Angebot(e) geändert Pushover versendet.`);
} catch (e) {
console.error('KöPi Tages-Check Fehler:', e.message);
}
}
// ── Routes ──────────────────────────────────────────────────────────────────── // ── Routes ────────────────────────────────────────────────────────────────────
router.get('/offers', authenticate, async (req, res) => { router.get('/offers', authenticate, async (req, res) => {
try { res.json(await scrapeMarktguru()); } try { res.json(await scrapeMarktguru()); }
@@ -274,3 +340,4 @@ router.post('/clear-cache', authenticate, (req, res) => {
}); });
module.exports = router; module.exports = router;
module.exports.runDailyCheck = runDailyCheck;