Fix Kalender-Suche: calendar_event_cache Tabelle, Sync-Endpoint, Dashboard-Trigger

This commit is contained in:
2026-06-05 16:19:17 +02:00
parent 3b13e1c292
commit 139f13e0aa
2 changed files with 69 additions and 54 deletions

View File

@@ -5,33 +5,45 @@ const db = require('../db');
const { authenticate } = require('../middleware/auth'); const { authenticate } = require('../middleware/auth');
const router = express.Router(); const router = express.Router();
const safe = (fn) => { try { return fn(); } catch { return []; } }; const safe = fn => { try { return fn(); } catch { return []; } };
const safeGet = (fn) => { try { return fn(); } catch { return null; } }; const safeGet = fn => { try { return fn(); } catch { return null; } };
// ── iCal-Fetch ──────────────────────────────────────────────────────────────── // ── iCal Fetch + Parse ────────────────────────────────────────────────────────
function fetchIcal(url) { function fetchIcal(url) {
return new Promise((resolve) => { return new Promise(resolve => {
try { try {
const lib = url.startsWith('https') ? https : http; const parsed = new URL(url);
const req = lib.get(url, { headers:{'User-Agent':'DickenDock/1.0'} }, res => { 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 = ''; 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)); res.on('end', () => resolve(data.includes('BEGIN:VCALENDAR') ? data : null));
}); });
req.on('error', () => resolve(null)); req.on('error', () => resolve(null));
req.setTimeout(8000, () => { req.destroy(); resolve(null); }); req.setTimeout(10000, () => { req.destroy(); resolve(null); });
} catch { resolve(null); } } catch { resolve(null); }
}); });
} }
function parseIcal(raw) { function parseIcal(raw) {
const events = []; const events = [];
const blocks = raw.split('BEGIN:VEVENT'); const parts = raw.split('BEGIN:VEVENT');
for (let i = 1; i < blocks.length; i++) { for (let i = 1; i < parts.length; i++) {
const block = blocks[i]; const b = parts[i];
const get = key => { const get = key => {
const m = block.match(new RegExp(key + '[^:]*:([^\\r\\n]+)')); // Handle folded lines and key variants like DTSTART;TZID=...
return m ? m[1].replace(/\\n/g,' ').replace(/\\,/g,',').trim() : ''; 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'); const summary = get('SUMMARY');
if (!summary) continue; if (!summary) continue;
@@ -40,29 +52,38 @@ function parseIcal(raw) {
return events; return events;
} }
// ── Kalender-Sync (awaitable, mit Timeout) ──────────────────────────────────── // ── Kalender-Sync Endpoint (POST /api/search/sync-calendar) ──────────────────
async function syncCalendarEvents(uid) { // Frontend ruft das beim Dashboard-Laden auf — kein Sync beim Suchen selbst
const feeds = db.prepare('SELECT * FROM calendar_feeds WHERE user_id=?').all(uid); router.post('/sync-calendar', authenticate, async (req, res) => {
if (!feeds.length) return; const uid = req.user.id;
const force = req.body?.force === true;
// Max alle 10 Min pro User sync 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(() => const lastSync = safeGet(() =>
db.prepare("SELECT MAX(synced_at) as s FROM calendar_event_cache WHERE user_id=?").get(uid)?.s db.prepare("SELECT MAX(synced_at) as s FROM calendar_event_cache WHERE user_id=?").get(uid)?.s
); );
if (lastSync) { if (lastSync && (Date.now() - new Date(lastSync).getTime()) < 10 * 60 * 1000) {
const age = Date.now() - new Date(lastSync).getTime(); return res.json({ ok: true, synced: 0, message: 'Cache noch frisch' });
if (age < 10 * 60 * 1000) return; // Noch frisch, kein Sync nötig }
} }
const ins = db.prepare(` 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')) VALUES (?,?,?,?,?,?,?,datetime('now','localtime'))
`); `);
let totalSynced = 0;
const errors = [];
for (const feed of feeds) { for (const feed of feeds) {
try { try {
const raw = await fetchIcal(feed.url); 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); const events = parseIcal(raw);
db.transaction(() => { db.transaction(() => {
db.prepare('DELETE FROM calendar_event_cache WHERE feed_id=?').run(feed.id); 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) 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 ──────────────────────────────────────────────────────────────────── res.json({ ok: true, synced: totalSynced, feeds: feeds.length, errors });
router.get('/', authenticate, async (req, res) => { });
// ── Suche ─────────────────────────────────────────────────────────────────────
router.get('/', authenticate, (req, res) => {
const q = (req.query.q || '').trim(); const q = (req.query.q || '').trim();
const uid = req.user.id; const uid = req.user.id;
@@ -88,14 +112,6 @@ router.get('/', authenticate, async (req, res) => {
const like = `%${q.toLowerCase()}%`; 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(` const links = safe(() => db.prepare(`
SELECT id, title, url, icon, description FROM link_list SELECT id, title, url, icon, description FROM link_list
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(url) LIKE ? OR LOWER(description) LIKE ?) 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(` const folders = safe(() => db.prepare(`
SELECT id, name, icon FROM link_list_folders SELECT id, name, icon FROM link_list_folders
WHERE user_id=? AND LOWER(name) LIKE ? WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5
ORDER BY name LIMIT 5
`).all(uid, like)); `).all(uid, like));
const snippets = safe(() => db.prepare(` const snippets = safe(() => db.prepare(`
@@ -116,14 +131,12 @@ router.get('/', authenticate, async (req, res) => {
const calculations = safe(() => db.prepare(` const calculations = safe(() => db.prepare(`
SELECT id, name FROM calculations SELECT id, name FROM calculations
WHERE user_id=? AND LOWER(name) LIKE ? WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5
ORDER BY name LIMIT 5
`).all(uid, like)); `).all(uid, like));
const orders = safe(() => db.prepare(` const orders = safe(() => db.prepare(`
SELECT id, name, status FROM orders SELECT id, name, status FROM orders
WHERE user_id=? AND LOWER(name) LIKE ? WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5
ORDER BY name LIMIT 5
`).all(uid, like)); `).all(uid, like));
const todos = safe(() => db.prepare(` const todos = safe(() => db.prepare(`
@@ -137,8 +150,8 @@ router.get('/', authenticate, async (req, res) => {
).get(uid, like)); ).get(uid, like));
const notes = noteRow ? [(() => { const notes = noteRow ? [(() => {
const idx = noteRow.content.toLowerCase().indexOf(q.toLowerCase()); const idx = noteRow.content.toLowerCase().indexOf(q.toLowerCase());
const start = Math.max(0, idx - 30); const s = Math.max(0, idx - 30);
return { id:'note', preview: (start>0?'…':'') + noteRow.content.slice(start, idx+60) + '…' }; return { id:'note', preview: (s>0?'…':'') + noteRow.content.slice(s, idx+60) + '…' };
})()] : []; })()] : [];
const board_items = safe(() => db.prepare(` const board_items = safe(() => db.prepare(`
@@ -147,7 +160,6 @@ router.get('/', authenticate, async (req, res) => {
ORDER BY created_at DESC LIMIT 5 ORDER BY created_at DESC LIMIT 5
`).all(uid, like, like)); `).all(uid, like, like));
// Changelog ist App-weit (kein user_id für alle Nutzer sichtbar)
const changelog = safe(() => db.prepare(` const changelog = safe(() => db.prepare(`
SELECT id, version, title, body FROM changelog SELECT id, version, title, body FROM changelog
WHERE LOWER(title) LIKE ? OR LOWER(body) LIKE ? WHERE LOWER(title) LIKE ? OR LOWER(body) LIKE ?
@@ -162,12 +174,11 @@ router.get('/', authenticate, async (req, res) => {
const file_folders = safe(() => db.prepare(` const file_folders = safe(() => db.prepare(`
SELECT id, name FROM folders SELECT id, name FROM folders
WHERE user_id=? AND LOWER(name) LIKE ? WHERE user_id=? AND LOWER(name) LIKE ? ORDER BY name LIMIT 5
ORDER BY name LIMIT 5
`).all(uid, like)); `).all(uid, like));
const push_schedules = safe(() => db.prepare(` 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 WHERE user_id=? AND LOWER(message) LIKE ? AND sent=0
ORDER BY scheduled_at LIMIT 5 ORDER BY scheduled_at LIMIT 5
`).all(uid, like)); `).all(uid, like));
@@ -178,14 +189,14 @@ router.get('/', authenticate, async (req, res) => {
ORDER BY name LIMIT 4 ORDER BY name LIMIT 4
`).all(uid, like, like)); `).all(uid, like, like));
// Gecachte Kalendereinträge nur eigene (user_id-gefiltert über JOIN)
const calendar_events = safe(() => db.prepare(` const calendar_events = safe(() => db.prepare(`
SELECT c.id, c.summary, c.description, c.location, c.start_dt, f.name as feed_name SELECT c.id, c.summary, c.description, c.location, c.start_dt, f.name as feed_name
FROM calendar_event_cache c FROM calendar_event_cache c
JOIN calendar_feeds f ON f.id=c.feed_id AND f.user_id=c.user_id JOIN calendar_feeds f ON f.id=c.feed_id
WHERE c.user_id=? AND (LOWER(c.summary) LIKE ? OR LOWER(c.description) LIKE ? OR LOWER(c.location) LIKE ?) 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 ORDER BY c.start_dt LIMIT 8
`).all(uid, like, like, like)); `).all(uid, uid, like, like, like));
const qr_codes = safe(() => db.prepare(` const qr_codes = safe(() => db.prepare(`
SELECT id, label, url FROM qr_codes SELECT id, label, url FROM qr_codes

View File

@@ -1873,6 +1873,10 @@ function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unr
const [showBoard, setShowBoard] = useState(false); const [showBoard, setShowBoard] = useState(false);
const [showChangelog, setShowChangelog] = useState(false); const [showChangelog, setShowChangelog] = useState(false);
useEffect(() => { const t = setInterval(() => setNow(new Date()), 30000); return () => clearInterval(t); }, []); 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 ( return (
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}> <div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
{showBoard && <BoardModal user={user} toast={toast} onClose={()=>setShowBoard(false)} onRead={onBoardRead}/>} {showBoard && <BoardModal user={user} toast={toast} onClose={()=>setShowBoard(false)} onRead={onBoardRead}/>}