diff --git a/.gitignore b/.gitignore index 7f40959..ab46839 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ docker-data/ # Misc *.tsbuildinfo next-env.d.ts + +# Monitoring secrets (generate at deploy) +monitoring/metrics-token diff --git a/architectural-grid/server.ts b/architectural-grid/server.ts index d1b46e5..2c6cb42 100644 --- a/architectural-grid/server.ts +++ b/architectural-grid/server.ts @@ -36,7 +36,7 @@ async function startServer() { const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); - const PORT = 3000; + const PORT = 4000; app.use(express.json()); diff --git a/architectural-grid/vite.config.ts b/architectural-grid/vite.config.ts index 0506f1b..9b26838 100644 --- a/architectural-grid/vite.config.ts +++ b/architectural-grid/vite.config.ts @@ -1,15 +1,11 @@ import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import path from 'path'; -import {defineConfig, loadEnv} from 'vite'; +import {defineConfig} from 'vite'; export default defineConfig(({mode}) => { - const env = loadEnv(mode, '.', ''); return { plugins: [react(), tailwindcss()], - define: { - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), - }, resolve: { alias: { '@': path.resolve(__dirname, '.'), diff --git a/architectural-grid1/server.ts b/architectural-grid1/server.ts index d1b46e5..2c6cb42 100644 --- a/architectural-grid1/server.ts +++ b/architectural-grid1/server.ts @@ -36,7 +36,7 @@ async function startServer() { const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); - const PORT = 3000; + const PORT = 4000; app.use(express.json()); diff --git a/architectural-grid1/vite.config.ts b/architectural-grid1/vite.config.ts index 0506f1b..3636b89 100644 --- a/architectural-grid1/vite.config.ts +++ b/architectural-grid1/vite.config.ts @@ -1,15 +1,11 @@ import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import path from 'path'; -import {defineConfig, loadEnv} from 'vite'; +import {defineConfig} from 'vite'; -export default defineConfig(({mode}) => { - const env = loadEnv(mode, '.', ''); +export default defineConfig(() => { return { plugins: [react(), tailwindcss()], - define: { - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), - }, resolve: { alias: { '@': path.resolve(__dirname, '.'), diff --git a/memento-mobile/assets/adaptive-icon.png b/memento-mobile/assets/adaptive-icon.png index 08cd6f2..6b825f4 100644 Binary files a/memento-mobile/assets/adaptive-icon.png and b/memento-mobile/assets/adaptive-icon.png differ diff --git a/memento-mobile/assets/icon.png b/memento-mobile/assets/icon.png index 08cd6f2..6b825f4 100644 Binary files a/memento-mobile/assets/icon.png and b/memento-mobile/assets/icon.png differ diff --git a/memento-mobile/assets/splash.png b/memento-mobile/assets/splash.png index 08cd6f2..6b825f4 100644 Binary files a/memento-mobile/assets/splash.png and b/memento-mobile/assets/splash.png differ diff --git a/memento-mobile/tailwind.config.js b/memento-mobile/tailwind.config.js index 0596dd2..701bd15 100644 --- a/memento-mobile/tailwind.config.js +++ b/memento-mobile/tailwind.config.js @@ -1,7 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'], - presets: [require('nativewind/preset')], theme: { extend: { colors: { diff --git a/memento-note/components/rich-text-editor.tsx b/memento-note/components/rich-text-editor.tsx index eff5cf4..c06c4cf 100644 --- a/memento-note/components/rich-text-editor.tsx +++ b/memento-note/components/rich-text-editor.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react' import { createPortal } from 'react-dom' import { useLanguage } from '@/lib/i18n' +import { sanitizeRichHtml } from '@/lib/sanitize-content' import { useEditor, EditorContent, useEditorState } from '@tiptap/react' import { BubbleMenu } from '@tiptap/react/menus' import StarterKit from '@tiptap/starter-kit' @@ -1572,7 +1573,7 @@ function BubbleToolbar({ editor, onSuggestCharts }: { editor: Editor | null; onS )}
{aiModal.type === 'preview' && {t('ai.result.suggestion') || 'Suggestion'}} -
+
diff --git a/memento-note/extension/dist-chrome-store/diagnose.js b/memento-note/extension/dist-chrome-store/diagnose.js new file mode 100644 index 0000000..b98cdf8 --- /dev/null +++ b/memento-note/extension/dist-chrome-store/diagnose.js @@ -0,0 +1,160 @@ +#!/usr/bin/env node +/** + * Script de diagnostic pour l'extension Momento + * Vérifie tous les fichiers et identifie les problèmes potentiels + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const extDir = __dirname + +console.log('🔍 Diagnostic Extension Momento\n') + +const issues = [] +const warnings = [] + +// Vérifier la syntaxe des fichiers JS +function checkSyntax(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf8') + // Pas de vérification syntaxique simple en Node.js sans eval + // On vérifie juste que le fichier est lisible + return true + } catch (error) { + issues.push(`Fichier illisible: ${filePath} - ${error.message}`) + return false + } +} + +// Vérifier les event handlers inline dans le HTML +function checkInlineHandlers(htmlPath) { + try { + const content = fs.readFileSync(htmlPath, 'utf8') + const inlineHandlers = [] + + if (content.match(/onerror=/i)) inlineHandlers.push('onerror') + if (content.match(/onclick=/i)) inlineHandlers.push('onclick') + if (content.match(/onload=/i)) inlineHandlers.push('onload') + + if (inlineHandlers.length > 0) { + issues.push(`Event handlers inline trouvés dans ${htmlPath}: ${inlineHandlers.join(', ')}`) + } else { + console.log('✓ Pas d\'event handlers inline dans le HTML') + } + } catch (error) { + issues.push(`Impossible de lire ${htmlPath}: ${error.message}`) + } +} + +// Vérifier les fixes CSP dans sidepanel.js +function checkCSPFixes(jsPath) { + try { + const content = fs.readFileSync(jsPath, 'utf8') + + // Vérifier l'absence de onerror inline + if (content.match(/onerror=/)) { + issues.push('Event handler onerror trouvé dans sidepanel.js') + } else { + console.log('✓ Pas de onerror inline dans sidepanel.js') + } + + // Vérifier la présence du fix avec data-fallback + if (content.includes('data-fallback')) { + console.log('✓ Fix CSP data-favicon présent') + } else { + warnings.push('Fix CSP data-favicon可能缺失') + } + + // Vérifier le handler de favicon + if (content.includes("querySelector('.page-favicon')")) { + console.log('✓ Handler error pour favicon présent') + } else { + warnings.push('Handler error pour favicon可能缺失') + } + + } catch (error) { + issues.push(`Impossible de lire ${jsPath}: ${error.message}`) + } +} + +// Vérifier le fix pick mode +function checkPickModeFix(jsPath) { + try { + const content = fs.readFileSync(jsPath, 'utf8') + + // Vérifier le handler visibilitychange + if (content.includes('visibilityState === \'hidden\'')) { + console.log('✓ Handler visibilitychange pour hidden présent') + } else { + issues.push('Handler visibilitychange pour hidden manquant') + } + + // Vérifier l'appel à setPickModeOnTab(false) + if (content.match(/visibilityState === 'hidden'.*setPickModeOnTab\(false\)/s)) { + console.log('✓ Appel setPickModeOnTab(false) dans visibilitychange présent') + } else { + issues.push('Appel setPickModeOnTab(false) dans visibilitychange可能缺失') + } + + } catch (error) { + issues.push(`Impossible de lire ${jsPath}: ${error.message}`) + } +} + +// Vérifier le manifest +function checkManifest(manifestPath) { + try { + const content = fs.readFileSync(manifestPath, 'utf8') + const manifest = JSON.parse(content) + + console.log('✓ Manifest.json valide') + console.log(` Version: ${manifest.version}`) + console.log(` Permissions: ${manifest.permissions.join(', ')}`) + console.log(` Host permissions: ${manifest.host_permissions.length}`) + + } catch (error) { + issues.push(`Manifest.json invalide: ${error.message}`) + } +} + +// Exécuter les tests +console.log('📋 Vérification des fichiers...\n') + +checkSyntax(path.join(extDir, 'sidepanel.js')) +checkSyntax(path.join(extDir, 'content.js')) +checkSyntax(path.join(extDir, 'background.js')) + +console.log('\n🔒 Vérification CSP...\n') +checkInlineHandlers(path.join(extDir, 'sidepanel.html')) +checkCSPFixes(path.join(extDir, 'sidepanel.js')) + +console.log('\n🎯 Vérification fix pick mode...\n') +checkPickModeFix(path.join(extDir, 'sidepanel.js')) + +console.log('\n📦 Vérification manifest...\n') +checkManifest(path.join(extDir, 'manifest.json')) + +// Résumé +console.log('\n' + '='.repeat(50)) +if (issues.length === 0 && warnings.length === 0) { + console.log('✅ Aucun problème détecté !') +} else { + if (issues.length > 0) { + console.log('\n❌ Problèmes détectés:') + issues.forEach(issue => console.log(` • ${issue}`)) + } + if (warnings.length > 0) { + console.log('\n⚠️ Warnings:') + warnings.forEach(warning => console.log(` • ${warning}`)) + } +} + +console.log('\n📝 Instructions:') +console.log('1. Rechargez l\'extension dans chrome://extensions (bouton 🔄)') +console.log('2. Ouvrez une page web normale') +console.log('3. Cliquez sur l\'icône Momento') +console.log('4. Fermez le sidepanel - la bannière doit disparaître') +console.log('5. Ouvrez la console (F12) - pas d\'erreur CSP') diff --git a/memento-note/extension/dist-chrome-store/manifest.json b/memento-note/extension/dist-chrome-store/manifest.json index 54d9395..9b1dfc5 100644 --- a/memento-note/extension/dist-chrome-store/manifest.json +++ b/memento-note/extension/dist-chrome-store/manifest.json @@ -13,8 +13,7 @@ ], "host_permissions": [ "https://memento-note.com/*", - "http://*/*", - "https://*/*" + "https://www.memento-note.com/*" ], "background": { "service_worker": "background.js" @@ -25,8 +24,8 @@ "content_scripts": [ { "matches": [ - "http://*/*", - "https://*/*" + "https://memento-note.com/*", + "https://www.memento-note.com/*" ], "js": [ "content.js" diff --git a/memento-note/extension/dist-chrome-store/memento-web-clipper-chrome-store.zip b/memento-note/extension/dist-chrome-store/memento-web-clipper-chrome-store.zip index 4973318..20eb25d 100644 Binary files a/memento-note/extension/dist-chrome-store/memento-web-clipper-chrome-store.zip and b/memento-note/extension/dist-chrome-store/memento-web-clipper-chrome-store.zip differ diff --git a/memento-note/extension/dist-chrome-store/sidepanel.js b/memento-note/extension/dist-chrome-store/sidepanel.js index c334286..eaa63b3 100644 --- a/memento-note/extension/dist-chrome-store/sidepanel.js +++ b/memento-note/extension/dist-chrome-store/sidepanel.js @@ -246,6 +246,14 @@ function bindIdleHandlers() { document.getElementById('clipSelBtn')?.addEventListener('click', () => void runAnalyze('selection')) document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page')) document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link')) + + // Gérer l'erreur de chargement du favicon + document.querySelector('.page-favicon')?.addEventListener('error', function() { + const fallback = this.getAttribute('data-fallback') + if (fallback && this.src !== fallback) { + this.src = fallback + } + }) } async function clearSelection() { @@ -410,7 +418,7 @@ function renderIdle() {
${escapeHtml(t('activePage'))}
- +
${escapeHtml(pageTitle || '—')}
${escapeHtml(pageUrl || '—')}
diff --git a/memento-note/extension/dist-chrome-store/test-sidepanel.html b/memento-note/extension/dist-chrome-store/test-sidepanel.html new file mode 100644 index 0000000..e8aa574 --- /dev/null +++ b/memento-note/extension/dist-chrome-store/test-sidepanel.html @@ -0,0 +1,78 @@ + + + + + Test Sidepanel + + + +

Test Extension Momento

+
+ + + + diff --git a/memento-note/extension/memento-web-clipper-chrome-store.zip b/memento-note/extension/memento-web-clipper-chrome-store.zip index 20eb25d..a23c08c 100644 Binary files a/memento-note/extension/memento-web-clipper-chrome-store.zip and b/memento-note/extension/memento-web-clipper-chrome-store.zip differ diff --git a/memento-note/extension/scripts/build-chrome-store.mjs b/memento-note/extension/scripts/build-chrome-store.mjs index 2754dca..8ba8c96 100755 --- a/memento-note/extension/scripts/build-chrome-store.mjs +++ b/memento-note/extension/scripts/build-chrome-store.mjs @@ -76,11 +76,13 @@ function processSidepanelJs(content) { function processManifestJson(content) { const manifest = JSON.parse(content) - // Remove localhost from host_permissions - if (manifest.host_permissions) { - manifest.host_permissions = manifest.host_permissions.filter( - perm => !perm.includes('localhost:3000') && !perm.includes('127.0.0.1:3000') - ) + manifest.host_permissions = [ + 'https://memento-note.com/*', + 'https://www.memento-note.com/*', + ] + + if (manifest.content_scripts?.[0]) { + manifest.content_scripts[0].matches = ['https://memento-note.com/*', 'https://www.memento-note.com/*'] } return JSON.stringify(manifest, null, 2) diff --git a/memento-note/lib/ai/services/semantic-search.service.ts b/memento-note/lib/ai/services/semantic-search.service.ts index c311d13..074a2b5 100644 --- a/memento-note/lib/ai/services/semantic-search.service.ts +++ b/memento-note/lib/ai/services/semantic-search.service.ts @@ -100,20 +100,60 @@ export class SemanticSearchService { const fusedResults = await this.reciprocalRankFusion(keywordResults, semanticResults) - return fusedResults + const topResults = fusedResults .sort((a, b) => b.score - a.score) .slice(0, opts.limit) - .map(result => ({ - ...result, - title: result.title || opts.defaultTitle, - matchType: result.score > 0.8 ? 'exact' as const : 'related' as const - })) + + // Fetch chunk snippets for top results (display only — no ranking change) + const noteIds = topResults.map(r => r.noteId) + const snippetMap = await this.fetchChunkSnippets(query, noteIds) + + return topResults.map(result => ({ + ...result, + title: result.title || opts.defaultTitle, + matchType: result.score > 0.8 ? 'exact' as const : 'related' as const, + matchedSnippets: snippetMap.get(result.noteId), + })) } catch (error) { console.error('Error in hybrid search:', error) return this._ftsFallback(query, userId, opts) } } + /** + * Fetch matching chunk snippets for display (no ranking impact). + * Finds chunks whose content contains the query terms. + */ + private async fetchChunkSnippets( + query: string, + noteIds: string[] + ): Promise> { + if (noteIds.length === 0 || !query.trim()) return new Map() + + try { + const rows: Array<{ noteId: string; content: string }> = await prisma.$queryRawUnsafe( + `SELECT "noteId", content FROM "NoteEmbeddingChunk" + WHERE "noteId" = ANY($1::text[]) + AND content ILIKE $2 + ORDER BY "noteId", "chunkIndex" + LIMIT 30`, + noteIds, + `%${query.slice(0, 100)}%` + ) + + const map = new Map() + for (const row of rows) { + const existing = map.get(row.noteId) || [] + const snippet = row.content.length > 200 ? row.content.slice(0, 200) + '...' : row.content + if (existing.length < 3) existing.push(snippet) + map.set(row.noteId, existing) + } + return map + } catch { + return new Map() + } + } + /** * PostgreSQL full-text search using tsvector + GIN index. * SECURITY: Uses $queryRawUnsafe with parameterized bind params ($1, $2…). diff --git a/memento-note/package.json b/memento-note/package.json index 468f01d..96d0a1e 100644 --- a/memento-note/package.json +++ b/memento-note/package.json @@ -14,7 +14,8 @@ "db:migrate:deploy": "prisma migrate deploy", "db:push": "prisma db push", "db:studio": "prisma studio", - "db:reset": "prisma migrate reset", + "db:reset": "node -e \"console.error('BLOCKED: use prisma migrate deploy. See CLAUDE.md'); process.exit(1)\"", + "db:reset:unsafe": "prisma migrate reset", "db:switch": "node scripts/switch-db.js", "setup:env": "node scripts/setup-env.js", "test": "playwright test", diff --git a/memento-note/scripts/migrate-chunk-embeddings.ts b/memento-note/scripts/migrate-chunk-embeddings.ts new file mode 100644 index 0000000..e068f7c --- /dev/null +++ b/memento-note/scripts/migrate-chunk-embeddings.ts @@ -0,0 +1,51 @@ +/** + * Script de migration : indexe toutes les notes existantes en fragments. + * Usage: npx tsx scripts/migrate-chunk-embeddings.ts + */ +import prisma from '@/lib/prisma' +import { chunkIndexingService } from '@/lib/ai/services/chunk-indexing.service' + +async function main() { + console.log('=== Migration des chunks d\'embeddings ===\n') + + const notes = await prisma.note.findMany({ + where: { + trashedAt: null, + noteEmbedding: { isNot: null }, + }, + select: { id: true, title: true, content: true }, + orderBy: { createdAt: 'asc' }, + }) + + console.log(`${notes.length} notes à indexer\n`) + + let done = 0 + let errors = 0 + const batchSize = 10 + + for (let i = 0; i < notes.length; i += batchSize) { + const batch = notes.slice(i, i + batchSize) + + await Promise.all(batch.map(async (note) => { + try { + const result = await chunkIndexingService.indexNote(note.id, note.title, note.content) + if (result.newFragments > 0) { + done++ + } + } catch (e: any) { + errors++ + console.error(` ✗ ${note.title?.slice(0, 40)}: ${e.message}`) + } + })) + + process.stdout.write(`\r${Math.min(i + batchSize, notes.length)}/${notes.length} traitées`) + } + + console.log(`\n\n=== Terminé ===`) + console.log(`Notes indexées: ${done}`) + console.log(`Erreurs: ${errors}`) + + await prisma.$disconnect() +} + +main().catch(console.error) diff --git a/monitoring/metrics-token.example b/monitoring/metrics-token.example new file mode 100644 index 0000000..4eacc13 --- /dev/null +++ b/monitoring/metrics-token.example @@ -0,0 +1,2 @@ +# Copy to metrics-token at deploy time (see scripts/deploy-prod.sh) +# Example: openssl rand -hex 32 > monitoring/metrics-token diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml index 26c977d..bd6a5ec 100644 --- a/monitoring/prometheus.yml +++ b/monitoring/prometheus.yml @@ -18,6 +18,11 @@ scrape_configs: static_configs: - targets: ['memento-note:3000'] + - job_name: 'mcp-server' + metrics_path: '/metrics' + static_configs: + - targets: ['mcp-server:3001'] + - job_name: 'node-exporter' static_configs: - targets: ['node-exporter:9100']