const express = require('express'); const https = require('https'); const http = require('http'); 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; } }; // ── iCal Fetch + Parse ──────────────────────────────────────────────────────── function fetchIcal(url) { return new Promise(resolve => { try { 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 > 1e6) { res.destroy(); resolve(null); } }); res.on('end', () => resolve(data.includes('BEGIN:VCALENDAR') ? data : null)); }); req.on('error', () => resolve(null)); req.setTimeout(10000, () => { req.destroy(); resolve(null); }); } catch { resolve(null); } }); } function parseIcal(raw) { const events = []; const parts = raw.split('BEGIN:VEVENT'); for (let i = 1; i < parts.length; i++) { const b = parts[i]; const get = key => { // 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; events.push({ summary, description:get('DESCRIPTION'), location:get('LOCATION'), start:get('DTSTART'), end:get('DTEND') }); } return events; } // ── 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; 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 // Alte Cache-Prüfung: Wenn noch historische Events drin (start_dt < 2000), force-sync const hasOldData = safeGet(() => db.prepare("SELECT 1 FROM calendar_event_cache WHERE user_id=? AND start_dt < '20200101' LIMIT 1").get(uid) ); if (!force && !hasOldData) { 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' }); } } let totalSynced = 0; const errors = []; for (const feed of feeds) { try { const raw = await fetchIcal(feed.url); if (!raw) { errors.push(`${feed.name}: kein gültiger iCal-Feed`); continue; } const events = parseIcal(raw); if (!events.length) { errors.push(`${feed.name}: 0 Events geparst`); continue; } // Nur zukünftige Events speichern (ab gestern rückwärts 1 Tag für Tagesevents) // start_dt Format: YYYYMMDD oder YYYYMMDDTHHmmssZ → string-vergleichbar const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const minDt = yesterday.toISOString().replace(/-/g,'').slice(0,8); // '20260604' const futureEvents = events.filter(ev => !ev.start || ev.start >= minDt); // prepare INSIDE try-catch – wirft wenn Tabelle nicht existiert 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(() => { db.prepare('DELETE FROM calendar_event_cache WHERE feed_id=?').run(feed.id); futureEvents.slice(0, 500).forEach(ev => ins.run(feed.id, uid, ev.summary||'', ev.description||'', ev.location||'', ev.start||'', ev.end||'') ); })(); totalSynced += futureEvents.length; } catch(e) { console.error(`[CalendarSync] Feed "${feed.name}" (${feed.url}): ${e.message}`); errors.push(`${feed.name}: ${e.message}`); } } console.log(`[CalendarSync] uid=${uid} synced=${totalSynced} feeds=${feeds.length} errors=${errors.length}`); 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; if (q.length < 2) return res.json({ links:[], folders:[], snippets:[], calculations:[], orders:[], todos:[], notes:[], board_items:[], changelog:[], files:[], file_folders:[], push_schedules:[], calendar_feeds:[], calendar_events:[], qr_codes:[] }); const like = `%${q.toLowerCase()}%`; 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 = 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 = 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 = 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 = 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 = 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)); const noteRow = safeGet(() => db.prepare( 'SELECT content FROM notes WHERE user_id=? AND LOWER(content) LIKE ?' ).get(uid, like)); const notes = noteRow ? [(() => { const idx = noteRow.content.toLowerCase().indexOf(q.toLowerCase()); 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(` 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 = 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 = 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 = 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 = safe(() => db.prepare(` 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)); 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 = 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 WHERE c.user_id=? AND f.user_id=? AND (LOWER(c.summary) LIKE ? OR LOWER(c.description) LIKE ? OR LOWER(c.location) LIKE ?) AND c.start_dt >= strftime('%Y%m%d', 'now') ORDER BY c.start_dt LIMIT 8 `).all(uid, uid, like, like, like)); 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 `).all(uid, like, like)); // Gebietseroberung-Spiele const geo_games = safe(() => db.prepare(` SELECT g.id, u1.username as owner_name, u2.username as opp_name, g.status, g.current_turn FROM geo_games g JOIN users u1 ON u1.id=g.owner_id JOIN users u2 ON u2.id=g.opponent_id WHERE (g.owner_id=? OR g.opponent_id=?) AND (LOWER(u1.username) LIKE ? OR LOWER(u2.username) LIKE ?) ORDER BY g.updated_at DESC LIMIT 5 `).all(uid, uid, like, like)); // Andere Benutzer – für Chat-Navigation (nur Benutzernamen, keine sensiblen Daten) const users = safe(() => db.prepare(` SELECT id, username FROM users WHERE id != ? AND LOWER(username) LIKE ? ORDER BY username LIMIT 5 `).all(uid, like)); res.json({ links, folders, snippets, calculations, orders, todos, notes, board_items, changelog, files, file_folders, push_schedules, calendar_feeds, calendar_events, qr_codes, users, geo_games }); }); // Debug-Endpoint: Kalender-Cache inspizieren router.get('/calendar-debug', authenticate, (req, res) => { const uid = req.user.id; const count = db.prepare('SELECT COUNT(*) as n FROM calendar_event_cache WHERE user_id=?').get(uid); const samples = db.prepare('SELECT summary, start_dt FROM calendar_event_cache WHERE user_id=? ORDER BY start_dt LIMIT 10').all(uid); const today = db.prepare("SELECT strftime('%Y%m%d','now') as d").get(); const future = db.prepare("SELECT COUNT(*) as n FROM calendar_event_cache WHERE user_id=? AND start_dt >= strftime('%Y%m%d','now')").get(uid); const search = db.prepare("SELECT summary, start_dt FROM calendar_event_cache WHERE user_id=? AND LOWER(summary) LIKE '%arzt%'").all(uid); res.json({ count: count.n, today: today.d, future_count: future.n, samples, arzt_matches: search }); }); module.exports = router;