243 lines
10 KiB
JavaScript
243 lines
10 KiB
JavaScript
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));
|
||
|
||
res.json({ links, folders, snippets, calculations, orders, todos, notes,
|
||
board_items, changelog, files, file_folders, push_schedules,
|
||
calendar_feeds, calendar_events, qr_codes });
|
||
});
|
||
|
||
// 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;
|