Fix: the server crashed on every Search; Search now covers tasks and events and explains itself
build-container / build (push) Successful in 1m55s

- Restore ddgAnswer, which an earlier edit deleted: /api/ddg threw a ReferenceError and killed the
  whole server process whenever Search ran
- One failing request can no longer take the server down (errors are logged and answered with a 500)
- tools/smoke-test.js starts the real server and calls every endpoint; the Gitea workflow runs it before
  building the image, so a missing function or a crash stops the build
- Search: also searches NE Hub tasks and calendar events; Seafile file search falls back to scanning file
  names when the search index returns nothing; Plex search falls back to the library's artists/albums;
  a status line shows what was searched, per-source counts and errors, with sign-in links

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-20 16:18:12 -04:00
1 parent ea882cafbf
commit 489ea0aaf0
8 files changed
+229 -40

No files matched your search

+18 -13
View File
@@ -247,6 +247,22 @@ const readBody = req => new Promise((ok, no) => { const c = []; req.on('data', d
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
const COLORS = ['#4f7cff', '#2f9e6e', '#e0742b', '#d9453a', '#8a5cf0', '#e5a00d'];
/** Every event in the window from all calendars (ICS links, iCloud, NE Hub): { events, errors } */
async function collectEvents(win) {
const all = [], errors = [];
await Promise.all(load().map(async c => {
try { for (const e of expand(parseEvents(await getIcs(c.url)), win)) all.push({ ...e, cal: c.name, color: c.color }); }
catch (e) { errors.push(`${c.name}: ${e.message}`); }
}));
try {
for (const r of await caldav.listRaw(win.from, win.to))
for (const e of expand(parseEvents(r.ics), win)) all.push({ ...e, cal: r.name, calId: r.calId, color: r.color, editable: true, href: r.href, etag: r.etag });
} catch (e) { errors.push('iCloud: ' + e.message); }
try { for (const e of expand(parseEvents(localIcs()), win)) all.push({ ...e, cal: LOCAL.name, calId: 'local', color: LOCAL.color, editable: true }); }
catch (e) { errors.push('NE Hub: ' + e.message); }
return { events: all, errors };
}
async function handle(req, res) {
const u = new URL(req.url, 'http://x');
try {
@@ -269,18 +285,7 @@ async function handle(req, res) {
const q = u.searchParams;
const win = { from: +q.get('from'), to: +q.get('to'), fd: q.get('fd'), td: q.get('td') };
if (!win.from || !win.to || !win.fd || !win.td) throw new Error('from/to required');
const all = [], errors = [];
await Promise.all(load().map(async c => {
try { for (const e of expand(parseEvents(await getIcs(c.url)), win)) all.push({ ...e, cal: c.name, color: c.color }); }
catch (e) { errors.push(`${c.name}: ${e.message}`); }
}));
try {
for (const r of await caldav.listRaw(win.from, win.to))
for (const e of expand(parseEvents(r.ics), win)) all.push({ ...e, cal: r.name, calId: r.calId, color: r.color, editable: true, href: r.href, etag: r.etag });
} catch (e) { errors.push('iCloud: ' + e.message); }
try { for (const e of expand(parseEvents(localIcs()), win)) all.push({ ...e, cal: LOCAL.name, calId: 'local', color: LOCAL.color, editable: true }); }
catch (e) { errors.push('NE Hub: ' + e.message); }
return json(res, 200, { events: all, errors });
return json(res, 200, await collectEvents(win));
}
if (u.pathname === '/api/cal/event' && req.method === 'GET') {
const q = u.searchParams, tz = q.get('tz') || 'UTC';
@@ -339,4 +344,4 @@ async function handle(req, res) {
} catch (e) { json(res, 400, { error: e.message }); }
}
module.exports = { unfold, parseLine, parseDate, unesc, handle, feed, parseEvents, expand, zonedToUtc, assertPublic, localIcs };
module.exports = { collectEvents, unfold, parseLine, parseDate, unesc, handle, feed, parseEvents, expand, zonedToUtc, assertPublic, localIcs };