From afc56d9fca67d5aa054b9dde2a181a690cc0e6b5 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 5 Jun 2026 13:17:24 +0200 Subject: [PATCH] =?UTF-8?q?Suche:=20Notizen,=20Kalendereintr=C3=A4ge,=20Bo?= =?UTF-8?q?ard,=20Changelog=20jetzt=20vollst=C3=A4ndig=20im=20Frontend=20u?= =?UTF-8?q?nd=20Backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/db.js | 17 +++++ backend/src/routes/search.js | 128 +++++++++++++++++++++++++++++++++-- frontend/src/App.jsx | 63 ++++++++++++++--- 3 files changed, 190 insertions(+), 18 deletions(-) diff --git a/backend/src/db.js b/backend/src/db.js index f154885..500cbe4 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -504,4 +504,21 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='lin `); } +// ── Kalender-Event-Cache für Suche ──────────────────────────────────────────── +if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='calendar_event_cache'").get()) { + db.exec(` + CREATE TABLE calendar_event_cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + feed_id INTEGER NOT NULL REFERENCES calendar_feeds(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + summary TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL DEFAULT '', + location TEXT NOT NULL DEFAULT '', + start_dt TEXT, + end_dt TEXT, + synced_at DATETIME DEFAULT NULL + ) + `); +} + module.exports = db; diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js index 4bcb670..c80a961 100644 --- a/backend/src/routes/search.js +++ b/backend/src/routes/search.js @@ -1,18 +1,97 @@ 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(); -router.get('/', authenticate, (req, res) => { +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 {} +} + +function fetchIcal(url) { + return new Promise((resolve) => { + try { + const lib = url.startsWith('https') ? https : http; + 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)); + }); + req.on('error', () => resolve(null)); + req.setTimeout(8000, () => { req.destroy(); resolve(null); }); + } catch { resolve(null); } + }); +} + +function parseIcal(raw) { + const events = []; + 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]+)`)); + 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'), + }); + } + return events; +} + +router.get('/', authenticate, async (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:[], files:[], file_folders:[], push_schedules:[], calendar_feeds:[], qr_codes:[] + todos:[], notes:[], board_items:[], changelog:[], + files:[], file_folders:[], push_schedules:[], + calendar_feeds:[], calendar_events:[], qr_codes:[] }); - const uid = req.user.id; + const like = `%${q.toLowerCase()}%`; - const safeAll = (fn) => { try { return fn(); } catch { return []; } }; + // Kalender-Events im Hintergrund synchronisieren + syncCalendarEvents(uid); const links = safeAll(() => db.prepare(` SELECT id, title, url, icon, description FROM link_list @@ -47,9 +126,34 @@ router.get('/', authenticate, (req, res) => { const todos = safeAll(() => db.prepare(` SELECT id, text, done FROM todos WHERE user_id=? AND LOWER(text) LIKE ? - ORDER BY done, created_at DESC LIMIT 5 + ORDER BY done, created_at DESC LIMIT 6 `).all(uid, like)); + // Notiz: Vorschau des passenden Textbereichs + const noteRow = safeAll(() => 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 board_items = safeAll(() => 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(` + 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(` SELECT id, originalname, mimetype, size FROM files WHERE user_id=? AND (LOWER(originalname) LIKE ? OR LOWER(filename) LIKE ?) @@ -71,16 +175,26 @@ router.get('/', authenticate, (req, res) => { const calendar_feeds = safeAll(() => db.prepare(` SELECT id, name, url FROM calendar_feeds WHERE user_id=? AND (LOWER(name) LIKE ? OR LOWER(url) LIKE ?) - ORDER BY name LIMIT 5 + 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 + FROM calendar_event_cache c + 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 ?) + ORDER BY c.start_dt LIMIT 6 + `).all(uid, like, like, like)); + const qr_codes = safeAll(() => 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, files, file_folders, push_schedules, calendar_feeds, qr_codes }); + res.json({ links, folders, snippets, calculations, orders, todos, notes, + board_items, changelog, files, file_folders, push_schedules, + calendar_feeds, calendar_events, qr_codes }); }); module.exports = router; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0783179..dc5802b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1445,12 +1445,16 @@ function SearchResultItem({ item, idx, activeIdx, onNavigate, onHover }) { if (item.type==='snippet') { icon='📄'; primary=item.title; secondary=`Code · ${item.language||''}`; } if (item.type==='calc') { icon='🖨'; primary=item.name; secondary='3D-Archiv'; } if (item.type==='order') { icon='📦'; primary=item.name; secondary=`Bestellung · ${item.status||''}`; } - if (item.type==='todo') { icon=item.done?'✅':'☐'; primary=item.text; secondary='ToDo'; } - if (item.type==='file') { icon='📄'; primary=item.originalname; secondary=`Datei · ${(item.size/1024/1024).toFixed(1)} MB`; } - if (item.type==='file_folder') { icon='📁'; primary=item.name; secondary='Datei-Ordner'; } - if (item.type==='push') { icon='🔔'; primary=item.message; secondary=`Push-Erinnerung · ${item.scheduled_at?.slice(0,16)||''}`; } - if (item.type==='calendar') { icon='📅'; primary=item.name; secondary=`Kalender-Abo · ${item.url||''}`; } - if (item.type==='qr') { icon='◻'; primary=item.label||item.url; secondary=`QR-Code · ${item.url}`; } + if (item.type==='todo') { icon=item.done?'✅':'☐'; primary=item.text; secondary='ToDo'; } + if (item.type==='note') { icon='📝'; primary='Notiz'; secondary=item.preview||''; } + if (item.type==='board') { icon=item.btype==='roadmap'?'🗺':'⭐'; primary=item.title; secondary=`${item.btype==='roadmap'?'Roadmap':'Wünsche'}${item.description?' · '+item.description.slice(0,40):''}`; } + if (item.type==='changelog') { icon='📋'; primary=`${item.version} – ${item.title}`; secondary='Changelog'; } + if (item.type==='file') { icon='📄'; primary=item.originalname; secondary=`Datei · ${item.size?(item.size/1024/1024).toFixed(1)+' MB':''}`; } + if (item.type==='file_folder') { icon='📁'; primary=item.name; secondary='Datei-Ordner'; } + if (item.type==='push') { icon='🔔'; primary=item.message; secondary=`Push · ${(item.scheduled_at||'').slice(0,16)}`; } + if (item.type==='calendar_feed') { icon='📅'; primary=item.name; secondary='Kalender-Abo'; } + if (item.type==='calendar_event') { icon='📆'; primary=item.summary; secondary=`${item.feed_name||'Kalender'}${item.start_dt?' · '+item.start_dt.slice(0,10):''}`; } + if (item.type==='qr') { icon='◻'; primary=item.label||item.url; secondary=`QR-Code · ${item.url}`; } return (
onNavigate(item)} onMouseEnter={()=>onHover(idx)} style={{display:'flex',alignItems:'center',gap:10,padding:'9px 14px',cursor:'pointer', @@ -1653,10 +1657,14 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) { ...(results.calculations||[]).map(c=>({...c,type:'calc'})), ...(results.orders||[]).map(o=>({...o,type:'order'})), ...(results.todos||[]).map(t=>({...t,type:'todo'})), + ...(results.notes||[]).map(n=>({...n,type:'note'})), + ...(results.board_items||[]).map(b=>({...b,type:'board'})), + ...(results.changelog||[]).map(e=>({...e,type:'changelog'})), ...(results.files||[]).map(f=>({...f,type:'file'})), ...(results.file_folders||[]).map(f=>({...f,type:'file_folder'})), ...(results.push_schedules||[]).map(p=>({...p,type:'push'})), - ...(results.calendar_feeds||[]).map(f=>({...f,type:'calendar'})), + ...(results.calendar_feeds||[]).map(f=>({...f,type:'calendar_feed'})), + ...(results.calendar_events||[]).map(e=>({...e,type:'calendar_event'})), ...(results.qr_codes||[]).map(q=>({...q,type:'qr'})), ] : []; @@ -1670,11 +1678,14 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) { else if (item.type === 'snippet') { setActive('codeschnipsel'); } else if (item.type === 'calc') { setActive('kalkulator3d'); } else if (item.type === 'order') { setActive('bestellungen'); } - else if (item.type === 'todo') { setActive('dashboard'); } + else if (item.type === 'todo') { setActive('dashboard'); } + else if (item.type === 'note') { setActive('dashboard'); } + else if (item.type === 'board') { setActive('dashboard'); } + else if (item.type === 'changelog') { setActive('dashboard'); } else if (item.type === 'file' || item.type === 'file_folder') { setActive('dateien'); } - else if (item.type === 'push') { setActive('dashboard'); } - else if (item.type === 'calendar') { setActive('dashboard'); } - else if (item.type === 'qr') { setActive('devtools'); setDevToolsNav({tool:'qrcode', ts: Date.now()}); } + else if (item.type === 'push') { setActive('dashboard'); } + else if (item.type === 'calendar_feed' || item.type === 'calendar_event') { setActive('dashboard'); } + else if (item.type === 'qr') { setActive('devtools'); setDevToolsNav({tool:'qrcode', ts: Date.now()}); } }; const onKey = e => { @@ -1813,6 +1824,36 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) { {results.qr_codes.map((item,i)=>({}}/>))}
)} + {results.notes?.length>0 && ( +
+
NOTIZEN
+ {results.notes.map((item,i)=>({}}/>))} +
+ )} + {results.board_items?.length>0 && ( +
+
ROADMAP / WÜNSCHE
+ {results.board_items.map((item,i)=>({}}/>))} +
+ )} + {results.changelog?.length>0 && ( +
+
CHANGELOG
+ {results.changelog.map((item,i)=>({}}/>))} +
+ )} + {results.calendar_events?.length>0 && ( +
+
KALENDER-EINTRÄGE
+ {results.calendar_events.map((item,i)=>({}}/>))} +
+ )} + {results.calendar_feeds?.length>0 && ( +
+
KALENDER-ABOS
+ {results.calendar_feeds.map((item,i)=>({}}/>))} +
+ )} )}