perf: optimize MCP server (O(1) auth, compact JSON, trashedAt fix) + memento-note performance (lazy loading, server-side filtering, XSS fixes, dead code removal, security hardening)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m35s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m35s
MCP Server: - Fix validateApiKey: O(1) direct lookup by shortId instead of loading all keys - Add trashedAt:null filter to ALL note queries (trashed notes leaked in results) - Compact JSON output (~40% smaller responses) - Bounded session cache (Map with MAX_SESSIONS=500) to prevent memory leaks - PostgreSQL connection pooling (connection_limit=10) - Rewrite all 22 tool descriptions in clear English - Fix /sse fallback to proper 307 redirect memento-note Performance: - loading=lazy on all note images - Split notebooksRefreshKey from global refreshKey (note CRUD no longer re-fetches notebooks) - Remove searchKey from trash count deps (no re-fetch on every keystroke) - Server-side notebookId filter in getAllNotes() (biggest win) - Skip collaborator fetch for non-shared notes (eliminates N+1 API calls) - next/dynamic for MarkdownContent + 4 modals (code-split remark/rehype/KaTeX) - Memoize DOMPurify sanitize with useMemo Security: - XSS: DOMPurify sanitize in note-card and note-history-modal - Auth anti-enumeration: uniform errors in auth.ts - CRON_SECRET mandatory on cron endpoints - Rate limiting on login (5 attempts/min per email) - Centralized API auth helpers (requireAuth/requireAdmin) - randomize-labels changed GET→POST - Removed debug endpoints (/api/debug/config, /api/debug/test-chat) Cleanup: - Removed dead code: .backup-keep, settings-backup, fix-*.js, debug-theme, fix-labels route - Removed sensitive console.error in auth.ts - Ollama fetchWithTimeout (30s/60s AbortController) - i18n: full Arabic translation, Farsi missing keys - Masonry drag-and-drop fix (localOrderMap, cross-section block) - Sidebar notebook tooltip on truncation
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const filePath = path.join(__dirname, '..', 'components', 'contextual-ai-chat.tsx');
|
||||
let src = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// ─── FIX 1: make inject buttons always visible on last assistant msg ──────────
|
||||
// Replace the hover-gated condition with always-visible for last msg
|
||||
const OLD_BUTTONS_CONDITION = ` {/* Hover-actions \u2014 visible only on assistant messages */}\r\n {isAssistant && onApplyToNote && (\r\n <div className={cn(\r\n 'flex gap-1 transition-all duration-150',\r\n isHovered ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-1 pointer-events-none',\r\n )}>`;
|
||||
|
||||
const NEW_BUTTONS_CONDITION = ` {/* Inject buttons \u2014 always visible on last assistant msg */}\r\n {isAssistant && onApplyToNote && (() => {\r\n const lastAssistantId = messages.filter(m => m.role === 'assistant').at(-1)?.id\r\n const alwaysShow = msg.id === lastAssistantId\r\n return (\r\n <div className={cn(\r\n 'flex gap-1 transition-all duration-150',\r\n (alwaysShow || isHovered) ? 'opacity-100' : 'opacity-0 pointer-events-none',\r\n )}>`;
|
||||
|
||||
// Also need to close the IIFE
|
||||
const OLD_BUTTONS_CLOSE = ` </div>\r\n )}\r\n </div>`;
|
||||
const NEW_BUTTONS_CLOSE = ` </div>\r\n )\r\n })()}\r\n </div>`;
|
||||
|
||||
if (src.includes(OLD_BUTTONS_CONDITION)) {
|
||||
src = src.replace(OLD_BUTTONS_CONDITION, NEW_BUTTONS_CONDITION);
|
||||
console.log('Fix 1a: buttons condition updated');
|
||||
} else {
|
||||
console.error('Fix 1a: OLD_BUTTONS_CONDITION not found!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (src.includes(OLD_BUTTONS_CLOSE)) {
|
||||
src = src.replace(OLD_BUTTONS_CLOSE, NEW_BUTTONS_CLOSE);
|
||||
console.log('Fix 1b: buttons IIFE close updated');
|
||||
} else {
|
||||
console.error('Fix 1b: OLD_BUTTONS_CLOSE not found!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ─── FIX 2: Move preview panel OUTSIDE the scrollable div ────────────────────
|
||||
// The scrollable div ends with: messagesEndRef then </div>
|
||||
// We need to:
|
||||
// A) Remove the preview block from inside the scrollable area (already was removed in previous sessions since it was added after messagesEndRef)
|
||||
// B) Add preview as shrink-0 panel between the scroll div and controls
|
||||
|
||||
// Find the closing of the scrollable area + start of controls
|
||||
const OLD_SCROLL_END = ` <div ref={messagesEndRef} />\r\n </div>\r\n\r\n {/* Scope & Tone Control Area */}`;
|
||||
const NEW_SCROLL_END = ` <div ref={messagesEndRef} />\r\n </div>\r\n\r\n {/* \u2550\u2550 Inject Preview Panel \u2014 fixed, always visible, NO scroll needed \u2550\u2550 */}\r\n {resourceEnriching && (\r\n <div className="shrink-0 mx-3 mb-2 flex items-center gap-2 rounded-xl border border-emerald-500/30 bg-emerald-50/50 dark:bg-emerald-950/20 p-3">\r\n <Loader2 className="h-3.5 w-3.5 animate-spin text-emerald-600 shrink-0" />\r\n <span className="text-xs text-emerald-700 dark:text-emerald-400">Traitement IA en cours...</span>\r\n </div>\r\n )}\r\n {resourcePreview && !resourceEnriching && (\r\n <div className="shrink-0 mx-3 mb-2 rounded-xl border border-primary/30 bg-primary/5 overflow-hidden">\r\n <div className="flex items-center justify-between px-3 py-2 border-b border-primary/20">\r\n <span className="text-[10px] font-semibold uppercase tracking-wider text-primary">\r\n {resourcePreview.source === 'complete' ? 'Compl\u00e9t\u00e9 par IA'\r\n : resourcePreview.source === 'merge' ? 'Fusionn\u00e9 par IA'\r\n : 'Aper\u00e7u'}\r\n </span>\r\n <button onClick={() => setResourcePreview(null)} className="text-muted-foreground hover:text-foreground">\r\n <X className="h-3 w-3" />\r\n </button>\r\n </div>\r\n <div className="px-3 py-2 max-h-36 overflow-y-auto text-sm">\r\n <MarkdownContent content={resourcePreview.text} />\r\n </div>\r\n <div className="flex gap-2 px-3 py-2 border-t border-primary/20">\r\n <button\r\n onClick={() => setResourcePreview(null)}\r\n className="flex-1 text-[11px] py-1.5 rounded-lg border border-border/60 text-muted-foreground hover:bg-muted transition-colors"\r\n >\r\n Annuler\r\n </button>\r\n <button\r\n onClick={() => {\r\n if (onApplyToNote && resourcePreview) {\r\n onApplyToNote(resourcePreview.text)\r\n setResourcePreview(null)\r\n setResourceText('')\r\n toast.success('Appliqu\u00e9 \u00e0 la note')\r\n }\r\n }}\r\n disabled={!onApplyToNote}\r\n className="flex-1 text-[11px] py-1.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center justify-center gap-1"\r\n >\r\n <Check className="h-3 w-3" />\r\n Appliquer \u00e0 la note\r\n </button>\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Scope & Tone Control Area */}`;
|
||||
|
||||
if (src.includes(OLD_SCROLL_END)) {
|
||||
src = src.replace(OLD_SCROLL_END, NEW_SCROLL_END);
|
||||
console.log('Fix 2: preview panel moved outside scroll');
|
||||
} else {
|
||||
console.error('Fix 2: OLD_SCROLL_END not found!');
|
||||
// Show what we're looking for context
|
||||
const idx = src.indexOf('messagesEndRef');
|
||||
console.error('Context around messagesEndRef:', JSON.stringify(src.slice(idx-20, idx+200)));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, src);
|
||||
console.log('Done!');
|
||||
@@ -1,51 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const filePath = path.join(__dirname, '..', 'components', 'contextual-ai-chat.tsx');
|
||||
let src = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Sync presets buttons
|
||||
const OLD_PRESET_CLICK = `onClick={() => setTranslateTarget(l)}`;
|
||||
const NEW_PRESET_CLICK = `onClick={() => { setTranslateTarget(l); setCustomLangInput(l); }}`;
|
||||
|
||||
if (src.includes(OLD_PRESET_CLICK)) {
|
||||
src = src.replace(OLD_PRESET_CLICK, NEW_PRESET_CLICK);
|
||||
console.log('Fix 1: Preset buttons sync updated');
|
||||
} else {
|
||||
console.error('Fix 1: OLD_PRESET_CLICK not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Sync input and enable live update
|
||||
const OLD_INPUT_LOGIC = `onChange={e => setCustomLangInput(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter' && customLangInput.trim()) { setTranslateTarget(customLangInput.trim()); setCustomLangInput('') } }}`;
|
||||
// Fallback for different line endings if needed, but let's try matching a smaller part first
|
||||
const OLD_ONCHANGE = `onChange={e => setCustomLangInput(e.target.value)}`;
|
||||
const NEW_ONCHANGE = `onChange={e => { const val = e.target.value; setCustomLangInput(val); setTranslateTarget(val); }}`;
|
||||
|
||||
const OLD_ONKEYDOWN = `onKeyDown={e => { if (e.key === 'Enter' && customLangInput.trim()) { setTranslateTarget(customLangInput.trim()); setCustomLangInput('') } }}`;
|
||||
const NEW_ONKEYDOWN = `onKeyDown={e => { if (e.key === 'Enter' && translateTarget.trim()) { handleAction(action, translateTarget.trim()); } }}`;
|
||||
|
||||
if (src.includes(OLD_ONCHANGE)) {
|
||||
src = src.replace(OLD_ONCHANGE, NEW_ONCHANGE);
|
||||
console.log('Fix 2: Input onChange updated');
|
||||
} else {
|
||||
console.error('Fix 2: OLD_ONCHANGE not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (src.includes(OLD_ONKEYDOWN)) {
|
||||
src = src.replace(OLD_ONKEYDOWN, NEW_ONKEYDOWN);
|
||||
console.log('Fix 3: Input onKeyDown updated');
|
||||
} else {
|
||||
console.error('Fix 3: OLD_ONKEYDOWN not found');
|
||||
// Try without \r if it failed
|
||||
const altOnKeyDown = OLD_ONKEYDOWN.replace('\r\n', '\n');
|
||||
if (src.includes(altOnKeyDown)) {
|
||||
src = src.replace(altOnKeyDown, NEW_ONKEYDOWN);
|
||||
console.log('Fix 3: Input onKeyDown updated (alt line ending)');
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, src);
|
||||
console.log('Done!');
|
||||
Reference in New Issue
Block a user