Fix: the server crashed on every Search; Search now covers tasks and events and explains itself
build-container / build (push) Successful in 1m55s
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:
1 parent
ea882cafbf
commit
489ea0aaf0
8 files changed
+229
-40
No files matched your search
@@ -13,6 +13,7 @@ const auth = require('./auth');
|
||||
const cal = require('./cal');
|
||||
const tasks = require('./tasks');
|
||||
const settings = require('./settings');
|
||||
const search = require('./search');
|
||||
const VERSE_URL = process.env.VERSE_URL || 'https://versebyversewithnate.us/admin';
|
||||
const PODCAST_URL = process.env.PODCAST_URL || 'https://podcast.necloud.us/';
|
||||
const PUBLIC = path.join(__dirname, 'public');
|
||||
@@ -290,9 +291,31 @@ const STARTED = Date.now();
|
||||
if (i.source && i.source.kind === 'anonymous') console.warn('[data] WARNING: /data is an anonymous Docker volume, not your host folder. It is deleted when the container is recreated. Check that your stack mounts the host folder at /data.');
|
||||
if (i.mounted === false) console.warn('[data] WARNING: the data folder is not a mounted volume. Passkeys, calendars and sessions will be lost whenever the container is recreated. Mount a volume or host folder at /data.'); }
|
||||
|
||||
// ---- DuckDuckGo instant answers for the Search tab (a short summary when DDG has one; many queries return nothing)
|
||||
const ddgCache = new Map();
|
||||
async function ddgAnswer(req, res) {
|
||||
const q = (new URL(req.url, 'http://x').searchParams.get('q') || '').trim().slice(0, 200);
|
||||
if (q.length < 2) return json(res, 200, {});
|
||||
const hit = ddgCache.get(q.toLowerCase());
|
||||
if (hit && Date.now() - hit.t < 10 * 60e3) return json(res, 200, hit.v);
|
||||
try {
|
||||
const u = new URL('https://api.duckduckgo.com/');
|
||||
u.search = new URLSearchParams({ q, format: 'json', no_html: '1', skip_disambig: '1', t: 'nehub' });
|
||||
const r = await fetch(u, { signal: AbortSignal.timeout(6000), headers: { Accept: 'application/json' } });
|
||||
const d = await r.json();
|
||||
const v = {};
|
||||
if (d.AbstractText) Object.assign(v, { heading: d.Heading, text: d.AbstractText.slice(0, 600), source: d.AbstractSource, url: d.AbstractURL, image: d.Image ? 'https://duckduckgo.com' + d.Image : '' });
|
||||
else if (d.Definition) Object.assign(v, { heading: d.Heading || q, text: d.Definition.slice(0, 600), source: d.DefinitionSource, url: d.DefinitionURL });
|
||||
if (d.Answer && typeof d.Answer === 'string') v.answer = d.Answer.slice(0, 200);
|
||||
if (ddgCache.size > 200) ddgCache.clear();
|
||||
ddgCache.set(q.toLowerCase(), { t: Date.now(), v });
|
||||
json(res, 200, v);
|
||||
} catch { json(res, 200, {}); } // a failed lookup must never break Search
|
||||
}
|
||||
|
||||
if (!JWT_SECRET) console.warn('WARNING: ONLYOFFICE_JWT_SECRET not set. In-app editing only works if the Document Server has JWT disabled, and save callbacks are unauthenticated. Testing only.');
|
||||
|
||||
http.createServer((req, res) => {
|
||||
async function handleRequest(req, res) {
|
||||
const pathname = new URL(req.url, 'http://x').pathname;
|
||||
if (pathname.startsWith('/api/auth/')) return auth.handle(req, res);
|
||||
if (pathname.startsWith('/cal/feed/')) return cal.feed(req, res); // subscribe feed: protected by its own secret token, not the session
|
||||
@@ -313,10 +336,22 @@ http.createServer((req, res) => {
|
||||
if (pathname === '/api/ddg') return ddgAnswer(req, res);
|
||||
if (pathname === '/api/system') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify(storageInfo())); }
|
||||
if (pathname === '/api/settings') return settings.handle(req, res);
|
||||
if (pathname === '/api/search') return search.handle(req, res);
|
||||
if (req.url.startsWith('/api/tasks')) return tasks.handle(req, res);
|
||||
if (req.url.startsWith('/api/cal/')) return cal.handle(req, res);
|
||||
if (req.url.startsWith('/api/plex/')) return plexApi(req, res);
|
||||
if (req.url.startsWith('/plex/') && req.method === 'GET') return plexProxy(req, res);
|
||||
if (req.url.startsWith('/sf/')) return proxySeafile(req, res);
|
||||
serveStatic(req, res);
|
||||
}
|
||||
|
||||
// A bug or bad input in one request must never take the whole server down: log it and answer that request with an error.
|
||||
http.createServer(async (req, res) => {
|
||||
try { await handleRequest(req, res); }
|
||||
catch (e) {
|
||||
console.error('[error]', req.method, req.url.split('?')[0], e && e.stack || e);
|
||||
if (!res.headersSent) { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'Server error: ' + (e && e.message || 'unknown') })); } else res.destroy();
|
||||
}
|
||||
}).listen(PORT, () => console.log(`Hub on :${PORT} (Seafile ${SEAFILE_URL})`));
|
||||
process.on('unhandledRejection', e => console.error('[unhandledRejection]', e && e.stack || e));
|
||||
process.on('uncaughtException', e => console.error('[uncaughtException]', e && e.stack || e));
|
||||
Reference in new issue
Block a user