diff --git a/server/src/dashboard.html b/server/src/dashboard.html
index 7a2c84d..f4ff042 100644
--- a/server/src/dashboard.html
+++ b/server/src/dashboard.html
@@ -972,37 +972,57 @@
return text.replace(regex, '$1');
}
+ let searchDebounceTimer = null;
+ let activeSearchAbortController = null;
+
async function handleSearch(e) {
- const q = e.target.value;
- // Provide live feedback if they type at least 3 characters, or hit Enter
- if (e.key === 'Enter' || q.length >= 3 || q.length === 0) {
- if (!q) {
- document.getElementById('search-results').innerHTML = '';
- return;
- }
- const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
- const data = await res.json();
- const container = document.getElementById('search-results');
-
- if (data.results.length === 0) {
- container.innerHTML = '
No results found.
';
- return;
- }
-
- container.innerHTML = data.results.map(r => `
-
-
-
- ${highlightText(r.title, q)}
- ${r.type_name}
-
-
${r.score.toFixed(2)}
-
-
${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? '...' : ''}
-
ID: ${r.id}
-
- `).join('');
+ const q = e.target.value.trim();
+
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
+
+ if (!q) {
+ if (activeSearchAbortController) activeSearchAbortController.abort();
+ document.getElementById('search-results').innerHTML = '';
+ return;
}
+
+ const delay = (e.key === 'Enter') ? 0 : 250;
+
+ searchDebounceTimer = setTimeout(async () => {
+ if (activeSearchAbortController) activeSearchAbortController.abort();
+ activeSearchAbortController = new AbortController();
+
+ try {
+ const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
+ signal: activeSearchAbortController.signal
+ });
+ const data = await res.json();
+ const container = document.getElementById('search-results');
+
+ if (!data.results || data.results.length === 0) {
+ container.innerHTML = 'No results found.
';
+ return;
+ }
+
+ container.innerHTML = data.results.map(r => `
+
+
+
+ ${highlightText(r.title, q)}
+ ${r.type_name}
+
+
${r.score.toFixed(2)}
+
+
${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? '...' : ''}
+
ID: ${r.id}
+
+ `).join('');
+ } catch (err) {
+ if (err.name !== 'AbortError') {
+ console.error("Search error:", err);
+ }
+ }
+ }, delay);
}
// --- WebSocket Activity Feed ---
@@ -1238,6 +1258,45 @@
}
}
+ function setupSSE() {
+ try {
+ const sse = new EventSource('/api/activity/stream');
+ sse.onmessage = function(event) {
+ if (event.data) {
+ try {
+ const rec = JSON.parse(event.data);
+ const feed = document.getElementById('activity-feed');
+ if (feed) {
+ const item = document.createElement('div');
+ item.style.padding = '8px 12px';
+ item.style.borderBottom = '1px solid var(--border-color)';
+ item.style.fontSize = '0.85em';
+ item.style.background = 'rgba(39, 174, 96, 0.08)';
+ item.innerHTML = `${rec.category || 'activity'}: ${rec.summary || event.data}`;
+ feed.insertBefore(item, feed.firstChild);
+ }
+ } catch(e) {}
+ }
+ };
+ } catch(e) { console.error('SSE initialization error', e); }
+ }
+
+ // Ctrl+K / Cmd+K Global Search shortcut
+ document.addEventListener('keydown', function(e) {
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
+ e.preventDefault();
+ const searchTabBtn = document.querySelectorAll('.tab-button')[1];
+ if (searchTabBtn) {
+ switchTab('search-tab', searchTabBtn);
+ }
+ const searchInput = document.getElementById('search-box');
+ if (searchInput) {
+ searchInput.focus();
+ searchInput.select();
+ }
+ }
+ });
+
// --- Start ---
loadVersion();
loadGraph();
@@ -1246,6 +1305,7 @@
loadActivityHistory();
loadAllExtras();
setupWS();
+ setupSSE();
// Listen to theme toggle changes to update graph font colors
const observer = new MutationObserver(() => updateGraphData());