Suche: Notizen, Kalendereinträge, Board, Changelog jetzt vollständig im Frontend und Backend
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1446,10 +1446,14 @@ function SearchResultItem({ item, idx, activeIdx, onNavigate, onHover }) {
|
||||
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==='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-Erinnerung · ${item.scheduled_at?.slice(0,16)||''}`; }
|
||||
if (item.type==='calendar') { icon='📅'; primary=item.name; secondary=`Kalender-Abo · ${item.url||''}`; }
|
||||
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 (
|
||||
<div onClick={()=>onNavigate(item)} onMouseEnter={()=>onHover(idx)}
|
||||
@@ -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'})),
|
||||
] : [];
|
||||
|
||||
@@ -1671,9 +1679,12 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
|
||||
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 === '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 === 'calendar_feed' || item.type === 'calendar_event') { setActive('dashboard'); }
|
||||
else if (item.type === 'qr') { setActive('devtools'); setDevToolsNav({tool:'qrcode', ts: Date.now()}); }
|
||||
};
|
||||
|
||||
@@ -1813,6 +1824,36 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
|
||||
{results.qr_codes.map((item,i)=>(<SearchResultItem key={`qr${i}`} item={{...item,type:'qr'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
||||
</div>
|
||||
)}
|
||||
{results.notes?.length>0 && (
|
||||
<div>
|
||||
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>NOTIZEN</div>
|
||||
{results.notes.map((item,i)=>(<SearchResultItem key={`n${i}`} item={{...item,type:'note'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
||||
</div>
|
||||
)}
|
||||
{results.board_items?.length>0 && (
|
||||
<div>
|
||||
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>ROADMAP / WÜNSCHE</div>
|
||||
{results.board_items.map((item,i)=>(<SearchResultItem key={`b${i}`} item={{...item,type:'board',btype:item.type}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
||||
</div>
|
||||
)}
|
||||
{results.changelog?.length>0 && (
|
||||
<div>
|
||||
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>CHANGELOG</div>
|
||||
{results.changelog.map((item,i)=>(<SearchResultItem key={`cl${i}`} item={{...item,type:'changelog'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
||||
</div>
|
||||
)}
|
||||
{results.calendar_events?.length>0 && (
|
||||
<div>
|
||||
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>KALENDER-EINTRÄGE</div>
|
||||
{results.calendar_events.map((item,i)=>(<SearchResultItem key={`ce${i}`} item={{...item,type:'calendar_event'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
||||
</div>
|
||||
)}
|
||||
{results.calendar_feeds?.length>0 && (
|
||||
<div>
|
||||
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>KALENDER-ABOS</div>
|
||||
{results.calendar_feeds.map((item,i)=>(<SearchResultItem key={`cf${i}`} item={{...item,type:'calendar_feed'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user