diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js index c80a961..696f66f 100644 --- a/backend/src/routes/search.js +++ b/backend/src/routes/search.js @@ -5,43 +5,10 @@ const db = require('../db'); const { authenticate } = require('../middleware/auth'); const router = express.Router(); -const safeAll = (fn) => { try { return fn(); } catch { return []; } }; - -// iCal-Feeds parsen und in Cache schreiben (max alle 10 Min pro User) -function syncCalendarEvents(uid) { - try { - const feeds = db.prepare('SELECT * FROM calendar_feeds WHERE user_id=?').all(uid); - if (!feeds.length) return; - // Prüfen ob sync nötig - const lastSync = 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; // < 10 Min: kein Sync nötig - } - - feeds.forEach(feed => { - try { - fetchIcal(feed.url).then(raw => { - if (!raw) return; - const events = parseIcal(raw); - db.prepare('DELETE FROM calendar_event_cache WHERE feed_id=?').run(feed.id); - const ins = db.prepare(` - INSERT INTO calendar_event_cache (feed_id, user_id, summary, description, location, start_dt, end_dt, synced_at) - VALUES (?,?,?,?,?,?,?,datetime('now','localtime')) - `); - db.transaction(() => { - events.slice(0, 200).forEach(ev => { - ins.run(feed.id, uid, ev.summary||'', ev.description||'', ev.location||'', ev.start||'', ev.end||''); - }); - })(); - }).catch(() => {}); - } catch {} - }); - } catch {} -} +const safe = (fn) => { try { return fn(); } catch { return []; } }; +const safeGet = (fn) => { try { return fn(); } catch { return null; } }; +// ── iCal-Fetch ──────────────────────────────────────────────────────────────── function fetchIcal(url) { return new Promise((resolve) => { try { @@ -49,7 +16,7 @@ function fetchIcal(url) { const req = lib.get(url, { headers:{'User-Agent':'DickenDock/1.0'} }, res => { let data = ''; res.on('data', d => { data += d; if (data.length > 500000) { res.destroy(); resolve(null); }}); - res.on('end', () => resolve(data)); + res.on('end', () => resolve(data.includes('BEGIN:VCALENDAR') ? data : null)); }); req.on('error', () => resolve(null)); req.setTimeout(8000, () => { req.destroy(); resolve(null); }); @@ -62,21 +29,52 @@ function parseIcal(raw) { const blocks = raw.split('BEGIN:VEVENT'); for (let i = 1; i < blocks.length; i++) { const block = blocks[i]; - const get = (key) => { - const m = block.match(new RegExp(`${key}[^:]*:([^\r\n]+)`)); + const get = key => { + const m = block.match(new RegExp(key + '[^:]*:([^\\r\\n]+)')); return m ? m[1].replace(/\\n/g,' ').replace(/\\,/g,',').trim() : ''; }; - events.push({ - summary: get('SUMMARY'), - description: get('DESCRIPTION'), - location: get('LOCATION'), - start: get('DTSTART'), - end: get('DTEND'), - }); + const summary = get('SUMMARY'); + if (!summary) continue; + events.push({ summary, description:get('DESCRIPTION'), location:get('LOCATION'), start:get('DTSTART'), end:get('DTEND') }); } 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; + + // 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 ins = db.prepare(` + INSERT INTO calendar_event_cache (feed_id, user_id, summary, description, location, start_dt, end_dt, synced_at) + VALUES (?,?,?,?,?,?,?,datetime('now','localtime')) + `); + + for (const feed of feeds) { + try { + const raw = await fetchIcal(feed.url); + if (!raw) continue; + const events = parseIcal(raw); + db.transaction(() => { + db.prepare('DELETE FROM calendar_event_cache WHERE feed_id=?').run(feed.id); + events.slice(0, 300).forEach(ev => + ins.run(feed.id, uid, ev.summary, ev.description, ev.location, ev.start, ev.end) + ); + })(); + } catch {} + } +} + +// ── Search ──────────────────────────────────────────────────────────────────── router.get('/', authenticate, async (req, res) => { const q = (req.query.q || '').trim(); const uid = req.user.id; @@ -90,103 +88,106 @@ router.get('/', authenticate, async (req, res) => { const like = `%${q.toLowerCase()}%`; - // Kalender-Events im Hintergrund synchronisieren - syncCalendarEvents(uid); + // 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 = safeAll(() => db.prepare(` + 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 ?) ORDER BY title LIMIT 8 `).all(uid, like, like, like)); - const folders = safeAll(() => db.prepare(` + 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 `).all(uid, like)); - const snippets = safeAll(() => db.prepare(` + const snippets = safe(() => db.prepare(` SELECT id, title, description, language FROM snippets WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ?) ORDER BY title LIMIT 6 `).all(uid, like, like)); - const calculations = safeAll(() => db.prepare(` + const calculations = safe(() => db.prepare(` SELECT id, name FROM calculations WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5 `).all(uid, like)); - const orders = safeAll(() => db.prepare(` + const orders = safe(() => db.prepare(` SELECT id, name, status FROM orders WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5 `).all(uid, like)); - const todos = safeAll(() => db.prepare(` + const todos = safe(() => db.prepare(` SELECT id, text, done FROM todos WHERE user_id=? AND LOWER(text) LIKE ? ORDER BY done, created_at DESC LIMIT 6 `).all(uid, like)); - // Notiz: Vorschau des passenden Textbereichs - const noteRow = safeAll(() => db.prepare( + const noteRow = safeGet(() => db.prepare( 'SELECT content FROM notes WHERE user_id=? AND LOWER(content) LIKE ?' ).get(uid, like)); - const notes = noteRow ? [{ - id: 'note', - preview: (() => { - const idx = noteRow.content.toLowerCase().indexOf(q.toLowerCase()); - const start = Math.max(0, idx - 30); - return (start > 0 ? '…' : '') + noteRow.content.slice(start, idx + 60) + '…'; - })() - }] : []; + 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 board_items = safeAll(() => db.prepare(` + const board_items = safe(() => db.prepare(` SELECT id, title, description, type FROM board_items WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ?) ORDER BY created_at DESC LIMIT 5 `).all(uid, like, like)); - const changelog = safeAll(() => db.prepare(` + // 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 ? ORDER BY created_at DESC LIMIT 4 `).all(like, like)); - const files = safeAll(() => db.prepare(` + const files = safe(() => db.prepare(` SELECT id, originalname, mimetype, size FROM files WHERE user_id=? AND (LOWER(originalname) LIKE ? OR LOWER(filename) LIKE ?) ORDER BY originalname LIMIT 6 `).all(uid, like, like)); - const file_folders = safeAll(() => db.prepare(` + const file_folders = safe(() => db.prepare(` SELECT id, name FROM folders WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5 `).all(uid, like)); - const push_schedules = safeAll(() => db.prepare(` + const push_schedules = safe(() => db.prepare(` SELECT id, message, scheduled_at, sent FROM push_schedules WHERE user_id=? AND LOWER(message) LIKE ? AND sent=0 ORDER BY scheduled_at LIMIT 5 `).all(uid, like)); - const calendar_feeds = safeAll(() => db.prepare(` + const calendar_feeds = safe(() => db.prepare(` SELECT id, name, url FROM calendar_feeds WHERE user_id=? AND (LOWER(name) LIKE ? OR LOWER(url) LIKE ?) ORDER BY name LIMIT 4 `).all(uid, like, like)); - const calendar_events = safeAll(() => db.prepare(` - SELECT c.id, c.summary, c.description, c.location, c.start_dt, f.name as feed_name, f.color + // 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 + 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 ?) - ORDER BY c.start_dt LIMIT 6 + ORDER BY c.start_dt LIMIT 8 `).all(uid, like, like, like)); - const qr_codes = safeAll(() => db.prepare(` + const qr_codes = safe(() => db.prepare(` SELECT id, label, url FROM qr_codes WHERE user_id=? AND (LOWER(label) LIKE ? OR LOWER(url) LIKE ?) ORDER BY created_at DESC LIMIT 5