Files
dickendock/backend/src/routes/search.js

243 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;