masked_name, e.id).toLowerCase(); const real = utils.safe(e.display?.real_name, '').toLowerCase(); const tagsStr = (e.tags || []).join(' ').toLowerCase(); const haystack = `${masked} ${real} ${e.id} ${tagsStr} ${e.type} ${e.layer}`.toLowerCase(); return haystack.includes(q); }).slice(0, 10); if (!matches.length) { resultsContainer.innerHTML = '
Aucun résultat trouvé.
'; return; } const showRealNames = state.get('showRealNames'); resultsContainer.innerHTML = `
${matches.length} résultat${matches.length > 1 ? 's' : ''}
${matches.map(e => { const name = showRealNames && e.display?.real_name ? e.display.real_name : e.display?.masked_name || e.id; const conf = Number.isFinite(+e.confidence) ? (+e.confidence).toFixed(2) : '—'; const confColor = getColorForConfidence(+e.confidence || 0.6); return `
${utils.escapeHtml(name)}
${utils.escapeHtml(e.type)} • conf: ${conf}
`; }).join('')}
`; // Bind click events resultsContainer.querySelectorAll('[data-select-id]').forEach(el => { el.addEventListener('click', () => { const id = el.getAttribute('data-select-id'); if (id) { selectEntity(id, { push: true }); setDrawer(true); } }); }); } // ============================================ // KEYBOARD SHORTCUTS // ============================================ document.addEventListener('keydown', (ev) => { // Escape: Close drawer if (ev.key === 'Escape' && state.get('drawerOpen')) { setDrawer(false); return; } // Home: Recenter view if (ev.key === 'Home') { ev.preventDefault(); state.set('view', { x: 0, y: 0, z: 1 }); document.getElementById('zoomSlider').value = 100; toast.success('Vue recentrée'); return; } // R: Regenerate positions if (ev.key === 'r' || ev.key === 'R') { if (!ev.ctrlKey && !ev.metaKey) { seedPositionsToZones(true); toast.success('Positions régénérées'); } return; } // M: Toggle mode if (ev.key === 'm' || ev.key === 'M') { const currentMode = state.get('mode'); const newMode = currentMode === 'ZONES' ? 'GRAPH' : 'ZONES'; state.set('mode', newMode); document.getElementById('modeBtnText').textContent = newMode; toast.success(`Mode ${newMode} activé`); return; } // Ctrl/Cmd + E: Export if ((ev.ctrlKey || ev.metaKey) && ev.key === 'e') { ev.preventDefault(); document.getElementById('exportBtn').click(); return; } // Alt + Left Arrow: Back in history if (ev.altKey && ev.key === 'ArrowLeft') { ev.preventDefault(); goHistory(-1); return; } // Alt + Right Arrow: Forward in history if (ev.altKey && ev.key === 'ArrowRight') { ev.preventDefault(); goHistory(+1); return; } // Ctrl/Cmd + K: Focus search if ((ev.ctrlKey || ev.metaKey) && ev.key === 'k') { ev.preventDefault(); setDrawer(true); setTimeout(() => { document.getElementById('quickSearch').focus(); }, 100); return; } }); // ============================================ // STATE SUBSCRIPTIONS & REACTIVITY // ============================================ state.subscribe('mode', () => { renderCanvas(); }); state.subscribe('view', () => { renderCanvas(); }); state.subscribe('showLabels', () => { renderCanvas(); }); state.subscribe('showOverlap', () => { renderCanvas(); }); state.subscribe('showRealNames', () => { renderCanvas(); }); state.subscribe('selectedId', () => { renderCanvas(); }); state.subscribe('hoverId', () => { renderCanvas(); }); state.subscribe('filter', () => { renderCanvas(); }); state.subscribe('quickSearch', () => { renderCanvas(); }); state.subscribe('drawerOpen', (open) => { if (!open) { renderCanvas(); } }); // ============================================ // PERSISTENCE & SESSION RESTORE // ============================================ function saveSession() { try { const sessionData = { savedAt: new Date().toISOString(), state: state.toJSON() }; localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(sessionData)); } catch (err) { console.warn('Could not save session:', err); } } function restoreSession() { try { const saved = localStorage.getItem(CONFIG.STORAGE_KEY); if (!saved) return false; const sessionData = JSON.parse(saved); const savedState = sessionData.state || {}; // Restore UI state if (savedState.mode) { state.set('mode', savedState.mode); document.getElementById('modeBtnText').textContent = savedState.mode; } if (savedState.theme) { document.documentElement.setAttribute('data-theme', savedState.theme); document.getElementById('themeSelect').value = savedState.theme; } if (savedState.locale) { document.getElementById('langSelect').value = savedState.locale; } if (savedState.view) { state.set('view', savedState.view); document.getElementById('zoomSlider').value = Math.round(savedState.view.z * 100); } if (savedState.showLabels !== undefined) { state.set('showLabels', savedState.showLabels); document.getElementById('labelsCheck').checked = savedState.showLabels; } if (savedState.showOverlap !== undefined) { state.set('showOverlap', savedState.showOverlap); document.getElementById('overlapCheck').checked = savedState.showOverlap; } if (savedState.showRealNames !== undefined) { state.set('showRealNames', savedState.showRealNames); document.getElementById('revealCheck').checked = savedState.showRealNames; } if (savedState.filter) { state.set('filter', savedState.filter); document.getElementById('filterInput').value = savedState.filter; } if (savedState.selectedId) { state.set('selectedId', savedState.selectedId); } if (savedState.navHistory) { state.set('navHistory', savedState.navHistory); } if (savedState.navIdx !== undefined) { state.set('navIdx', savedState.navIdx); } console.log('✅ Session restored'); updateNavButtons(); renderDetailPane(); return true; } catch (err) { console.warn('Could not restore session:', err); return false; } } // Autosave every 5 seconds setInterval(() => { saveSession(); }, 5000); // Save on unload window.addEventListener('beforeunload', () => { saveSession(); }); // ============================================ // INITIALIZATION // ============================================ async function init() { console.log('🚀 Initializing NYXO Portal v2.0...'); // Restore UI preferences from localStorage const savedTheme = localStorage.getItem('nyxo.theme'); if (savedTheme) { document.documentElement.setAttribute('data-theme', savedTheme); document.getElementById('themeSelect').value = savedTheme; state.set('theme', savedTheme); } const savedLocale = localStorage.getItem('nyxo.locale'); if (savedLocale) { document.getElementById('langSelect').value = savedLocale; state.set('locale', savedLocale); } // Restore session state restoreSession(); // Load data from IndexedDB await loadData(); // Initial render renderCanvas(); renderDetailPane(); console.log('✅ NYXO Portal ready!'); // Show welcome toast setTimeout(() => { toast.success('🌟 Bienvenue sur NYXO Portal'); }, 500); } // Start the app init().catch(err => { console.error('Fatal error during initialization:', err); toast.error('Erreur fatale lors de l\'initialisation'); }); // ============================================ // SERVICE WORKER (PWA) // ============================================ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('✅ Service Worker registered:', registration); }) .catch(err => { console.warn('Service Worker registration failed:', err); }); }); } // ============================================ // ANALYTICS (Privacy-first, opt-in only) // ============================================ // TODO: Implement privacy-respecting analytics // - No tracking by default // - Opt-in only // - Anonymized // - Self-hosted (Plausible or Umami) console.log('%cBuilt with ❤️ for Brussels mental health community', 'color: #d7a8ff; font-style: italic;'); console.log('%cContribute: https://github.com/nyxo-brussels', 'color: #86fbd6;');