From 1900d734900d8d5cf3dc0f745659b6407f179565 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 10 Jul 2026 13:26:33 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20K=C3=B6Pi=20Tages-Cron=20um=2006:00=20U?= =?UTF-8?q?hr=20mit=20Pushover-Benachrichtigung=20bei=20Angebots=C3=A4nder?= =?UTF-8?q?ung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/index.js | 15 +++++++ backend/src/tools/koepi/routes.js | 73 +++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/backend/src/index.js b/backend/src/index.js index 95b66f9..ab96910 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -256,6 +256,7 @@ server.listen(4000, () => console.log('πŸš€ Dicken Dock lΓ€uft auf Port 4000')); // ── Push-Zeitplaner Hintergrund-Job ────────────────────────────────────────── const db = require('./db'); +const koepiRoutes = require('./tools/koepi/routes'); async function sendPushoverMsg(userKey, appToken, message, opts = {}) { try { @@ -296,6 +297,20 @@ async function runScheduler() { 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); } + + // 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`); diff --git a/backend/src/tools/koepi/routes.js b/backend/src/tools/koepi/routes.js index 2a4debb..d2a0b53 100644 --- a/backend/src/tools/koepi/routes.js +++ b/backend/src/tools/koepi/routes.js @@ -1,5 +1,6 @@ const express = require('express'); const https = require('https'); +const db = require('../../db'); const { authenticate } = require('../../middleware/auth'); const router = express.Router(); @@ -40,10 +41,12 @@ function getAddress(retailer) { } // ── Angebote via Puppeteer von marktguru ───────────────────────────────────── -async function scrapeMarktguru() { +async function scrapeMarktguru(forceRefresh = false) { const cacheKey = 'koepi:mg'; - const cached = cacheGet(cacheKey); - if (cached) return cached; + if (!forceRefresh) { + const cached = cacheGet(cacheKey); + if (cached) return cached; + } let puppeteer; try { puppeteer = require('puppeteer-core'); } @@ -256,6 +259,69 @@ router.get('/img', authenticate, (req, res) => { } 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 ──────────────────────────────────────────────────────────────────── router.get('/offers', authenticate, async (req, res) => { try { res.json(await scrapeMarktguru()); } @@ -274,3 +340,4 @@ router.post('/clear-cache', authenticate, (req, res) => { }); module.exports = router; +module.exports.runDailyCheck = runDailyCheck;