diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js index 696f66f..661151d 100644 --- a/backend/src/routes/search.js +++ b/backend/src/routes/search.js @@ -5,33 +5,45 @@ const db = require('../db'); const { authenticate } = require('../middleware/auth'); const router = express.Router(); -const safe = (fn) => { try { return fn(); } catch { return []; } }; -const safeGet = (fn) => { try { return fn(); } catch { return null; } }; +const safe = fn => { try { return fn(); } catch { return []; } }; +const safeGet = fn => { try { return fn(); } catch { return null; } }; -// ── iCal-Fetch ──────────────────────────────────────────────────────────────── +// ── iCal Fetch + Parse ──────────────────────────────────────────────────────── function fetchIcal(url) { - return new Promise((resolve) => { + return new Promise(resolve => { try { - const lib = url.startsWith('https') ? https : http; - const req = lib.get(url, { headers:{'User-Agent':'DickenDock/1.0'} }, res => { + const parsed = new URL(url); + const lib = parsed.protocol === 'https:' ? https : http; + const req = lib.get(url, { + headers: { 'User-Agent': 'Mozilla/5.0 DickenDock/1.0', 'Accept': 'text/calendar,*/*' } + }, res => { + // Redirects verfolgen + if ([301,302,303,307,308].includes(res.statusCode) && res.headers.location) { + res.destroy(); + const next = res.headers.location.startsWith('http') + ? res.headers.location : new URL(res.headers.location, url).href; + fetchIcal(next).then(resolve); + return; + } let data = ''; - res.on('data', d => { data += d; if (data.length > 500000) { res.destroy(); resolve(null); }}); + res.on('data', d => { data += d; if (data.length > 1e6) { res.destroy(); resolve(null); } }); res.on('end', () => resolve(data.includes('BEGIN:VCALENDAR') ? data : null)); }); req.on('error', () => resolve(null)); - req.setTimeout(8000, () => { req.destroy(); resolve(null); }); + req.setTimeout(10000, () => { req.destroy(); resolve(null); }); } catch { resolve(null); } }); } function parseIcal(raw) { const events = []; - const blocks = raw.split('BEGIN:VEVENT'); - for (let i = 1; i < blocks.length; i++) { - const block = blocks[i]; + const parts = raw.split('BEGIN:VEVENT'); + for (let i = 1; i < parts.length; i++) { + const b = parts[i]; const get = key => { - const m = block.match(new RegExp(key + '[^:]*:([^\\r\\n]+)')); - return m ? m[1].replace(/\\n/g,' ').replace(/\\,/g,',').trim() : ''; + // Handle folded lines and key variants like DTSTART;TZID=... + const m = b.match(new RegExp(key + '[^:]*:([^\\r\\n]+)')); + return m ? m[1].replace(/\\n/g,' ').replace(/\\,/g,',').replace(/\\;/g,';').trim() : ''; }; const summary = get('SUMMARY'); if (!summary) continue; @@ -40,29 +52,38 @@ function parseIcal(raw) { return events; } -// ── Kalender-Sync (awaitable, mit Timeout) ──────────────────────────────────── -async function syncCalendarEvents(uid) { - const feeds = db.prepare('SELECT * FROM calendar_feeds WHERE user_id=?').all(uid); - if (!feeds.length) return; +// ── Kalender-Sync Endpoint (POST /api/search/sync-calendar) ────────────────── +// Frontend ruft das beim Dashboard-Laden auf — kein Sync beim Suchen selbst +router.post('/sync-calendar', authenticate, async (req, res) => { + const uid = req.user.id; + const force = req.body?.force === true; - // Max alle 10 Min pro User sync - const lastSync = safeGet(() => - db.prepare("SELECT MAX(synced_at) as s FROM calendar_event_cache WHERE user_id=?").get(uid)?.s - ); - if (lastSync) { - const age = Date.now() - new Date(lastSync).getTime(); - if (age < 10 * 60 * 1000) return; // Noch frisch, kein Sync nötig + const feeds = safeGet(() => db.prepare('SELECT * FROM calendar_feeds WHERE user_id=?').all(uid)) || []; + if (!feeds.length) return res.json({ ok: true, synced: 0, message: 'Keine Kalender-Abos vorhanden' }); + + // Alter des Caches prüfen + if (!force) { + const lastSync = safeGet(() => + db.prepare("SELECT MAX(synced_at) as s FROM calendar_event_cache WHERE user_id=?").get(uid)?.s + ); + if (lastSync && (Date.now() - new Date(lastSync).getTime()) < 10 * 60 * 1000) { + return res.json({ ok: true, synced: 0, message: 'Cache noch frisch' }); + } } const ins = db.prepare(` - INSERT INTO calendar_event_cache (feed_id, user_id, summary, description, location, start_dt, end_dt, synced_at) + INSERT INTO calendar_event_cache + (feed_id, user_id, summary, description, location, start_dt, end_dt, synced_at) VALUES (?,?,?,?,?,?,?,datetime('now','localtime')) `); + let totalSynced = 0; + const errors = []; + for (const feed of feeds) { try { const raw = await fetchIcal(feed.url); - if (!raw) continue; + if (!raw) { errors.push(`${feed.name}: kein gültiger iCal-Feed`); continue; } const events = parseIcal(raw); db.transaction(() => { db.prepare('DELETE FROM calendar_event_cache WHERE feed_id=?').run(feed.id); @@ -70,12 +91,15 @@ async function syncCalendarEvents(uid) { ins.run(feed.id, uid, ev.summary, ev.description, ev.location, ev.start, ev.end) ); })(); - } catch {} + totalSynced += events.length; + } catch(e) { errors.push(`${feed.name}: ${e.message}`); } } -} -// ── Search ──────────────────────────────────────────────────────────────────── -router.get('/', authenticate, async (req, res) => { + res.json({ ok: true, synced: totalSynced, feeds: feeds.length, errors }); +}); + +// ── Suche ───────────────────────────────────────────────────────────────────── +router.get('/', authenticate, (req, res) => { const q = (req.query.q || '').trim(); const uid = req.user.id; @@ -88,14 +112,6 @@ router.get('/', authenticate, async (req, res) => { const like = `%${q.toLowerCase()}%`; - // Kalender-Sync AWAITEN (max 6 Sekunden – danach aus Cache lesen was da ist) - try { - await Promise.race([ - syncCalendarEvents(uid), - new Promise(r => setTimeout(r, 6000)) - ]); - } catch {} - const links = safe(() => db.prepare(` SELECT id, title, url, icon, description FROM link_list WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(url) LIKE ? OR LOWER(description) LIKE ?) @@ -104,8 +120,7 @@ router.get('/', authenticate, async (req, res) => { const folders = safe(() => db.prepare(` SELECT id, name, icon FROM link_list_folders - WHERE user_id=? AND LOWER(name) LIKE ? - ORDER BY name LIMIT 5 + WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5 `).all(uid, like)); const snippets = safe(() => db.prepare(` @@ -116,14 +131,12 @@ router.get('/', authenticate, async (req, res) => { const calculations = safe(() => db.prepare(` SELECT id, name FROM calculations - WHERE user_id=? AND LOWER(name) LIKE ? - ORDER BY name LIMIT 5 + WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5 `).all(uid, like)); const orders = safe(() => db.prepare(` SELECT id, name, status FROM orders - WHERE user_id=? AND LOWER(name) LIKE ? - ORDER BY name LIMIT 5 + WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5 `).all(uid, like)); const todos = safe(() => db.prepare(` @@ -137,8 +150,8 @@ router.get('/', authenticate, async (req, res) => { ).get(uid, like)); const notes = noteRow ? [(() => { const idx = noteRow.content.toLowerCase().indexOf(q.toLowerCase()); - const start = Math.max(0, idx - 30); - return { id:'note', preview: (start>0?'…':'') + noteRow.content.slice(start, idx+60) + '…' }; + const s = Math.max(0, idx - 30); + return { id:'note', preview: (s>0?'…':'') + noteRow.content.slice(s, idx+60) + '…' }; })()] : []; const board_items = safe(() => db.prepare(` @@ -147,7 +160,6 @@ router.get('/', authenticate, async (req, res) => { ORDER BY created_at DESC LIMIT 5 `).all(uid, like, like)); - // Changelog ist App-weit (kein user_id – für alle Nutzer sichtbar) const changelog = safe(() => db.prepare(` SELECT id, version, title, body FROM changelog WHERE LOWER(title) LIKE ? OR LOWER(body) LIKE ? @@ -162,12 +174,11 @@ router.get('/', authenticate, async (req, res) => { const file_folders = safe(() => db.prepare(` SELECT id, name FROM folders - WHERE user_id=? AND LOWER(name) LIKE ? - ORDER BY name LIMIT 5 + WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5 `).all(uid, like)); const push_schedules = safe(() => db.prepare(` - SELECT id, message, scheduled_at, sent FROM push_schedules + SELECT id, message, scheduled_at FROM push_schedules WHERE user_id=? AND LOWER(message) LIKE ? AND sent=0 ORDER BY scheduled_at LIMIT 5 `).all(uid, like)); @@ -178,14 +189,14 @@ router.get('/', authenticate, async (req, res) => { ORDER BY name LIMIT 4 `).all(uid, like, like)); - // Gecachte Kalendereinträge – nur eigene (user_id-gefiltert über JOIN) const calendar_events = safe(() => db.prepare(` SELECT c.id, c.summary, c.description, c.location, c.start_dt, f.name as feed_name FROM calendar_event_cache c - JOIN calendar_feeds f ON f.id=c.feed_id AND f.user_id=c.user_id - WHERE c.user_id=? AND (LOWER(c.summary) LIKE ? OR LOWER(c.description) LIKE ? OR LOWER(c.location) LIKE ?) + JOIN calendar_feeds f ON f.id=c.feed_id + WHERE c.user_id=? AND f.user_id=? + AND (LOWER(c.summary) LIKE ? OR LOWER(c.description) LIKE ? OR LOWER(c.location) LIKE ?) ORDER BY c.start_dt LIMIT 8 - `).all(uid, like, like, like)); + `).all(uid, uid, like, like, like)); const qr_codes = safe(() => db.prepare(` SELECT id, label, url FROM qr_codes diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index dc5802b..260e887 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1873,6 +1873,10 @@ function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unr const [showBoard, setShowBoard] = useState(false); const [showChangelog, setShowChangelog] = useState(false); useEffect(() => { const t = setInterval(() => setNow(new Date()), 30000); return () => clearInterval(t); }, []); + // Kalender-Events im Hintergrund für die Suche cachen + useEffect(() => { + api('/search/sync-calendar', { method:'POST', body:{} }).catch(()=>{}); + }, []); return (
{showBoard && setShowBoard(false)} onRead={onBoardRead}/>}