feat: chunks recherche (snippets) + script migration
1. Recherche: fetchChunkSnippets() — après le classement RRF existant, récupère les passages précis qui matchent depuis NoteEmbeddingChunk. Pur affichage, AUCUN changement de classement. 2. Script migration: scripts/migrate-chunk-embeddings.ts Indexe toutes les notes existantes en fragments. Batch de 10, barre de progression. Usage: npx tsx scripts/migrate-chunk-embeddings.ts 3. Memory Echo chunk-level: à faire (US restante)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -50,3 +50,6 @@ docker-data/
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Monitoring secrets (generate at deploy)
|
||||
monitoring/metrics-token
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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, '.'),
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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, '.'),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 31 KiB |
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
<div className="notion-ai-result-section">
|
||||
{aiModal.type === 'preview' && <span className="notion-ai-result-label">{t('ai.result.suggestion') || 'Suggestion'}</span>}
|
||||
<div className="prose prose-sm max-w-none text-sm mt-1" dangerouslySetInnerHTML={{ __html: aiModal.html }} />
|
||||
<div className="prose prose-sm max-w-none text-sm mt-1" dangerouslySetInnerHTML={{ __html: sanitizeRichHtml(aiModal.html) }} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-3 pt-3 border-t border-border/40">
|
||||
<button onClick={() => setAiModal(null)} className="notion-modal-btn">{t('common.cancel') || 'Annuler'}</button>
|
||||
|
||||
160
memento-note/extension/dist-chrome-store/diagnose.js
Normal file
160
memento-note/extension/dist-chrome-store/diagnose.js
Normal file
@@ -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')
|
||||
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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() {
|
||||
<div class="page-card">
|
||||
<span class="sub">${escapeHtml(t('activePage'))}</span>
|
||||
<div class="page-row">
|
||||
<img src="${escapeHtml(pageFavicon)}" alt="" onerror="this.src='https://www.google.com/s2/favicons?domain=google.com&sz=32'" />
|
||||
<img src="${escapeHtml(pageFavicon)}" alt="" class="page-favicon" data-fallback="https://www.google.com/s2/favicons?domain=google.com&sz=32" />
|
||||
<div class="page-text">
|
||||
<div class="title"${rtlAttrs(pageTitle)}>${escapeHtml(pageTitle || '—')}</div>
|
||||
<div class="url">${escapeHtml(pageUrl || '—')}</div>
|
||||
|
||||
78
memento-note/extension/dist-chrome-store/test-sidepanel.html
Normal file
78
memento-note/extension/dist-chrome-store/test-sidepanel.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test Sidepanel</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; }
|
||||
.error { color: red; }
|
||||
.success { color: green; }
|
||||
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Extension Momento</h1>
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
const results = document.getElementById('results');
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const div = document.createElement('div');
|
||||
div.className = type;
|
||||
div.textContent = message;
|
||||
results.appendChild(div);
|
||||
}
|
||||
|
||||
function testFile(filename) {
|
||||
return fetch(filename)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.text();
|
||||
})
|
||||
.then(content => {
|
||||
log(`✓ ${filename} chargé (${content.length} bytes)`, 'success');
|
||||
return { filename, content };
|
||||
})
|
||||
.catch(error => {
|
||||
log(`✗ ${filename}: ${error.message}`, 'error');
|
||||
return { filename, error };
|
||||
});
|
||||
}
|
||||
|
||||
async function testSidepanelJS() {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'sidepanel.js';
|
||||
script.onerror = () => log('✗ sidepanel.js: Erreur de chargement', 'error');
|
||||
script.onload = () => {
|
||||
log('✓ sidepanel.js chargé', 'success');
|
||||
// Check for CSP violations
|
||||
if (typeof chrome !== 'undefined') {
|
||||
log('✓ API chrome disponible', 'success');
|
||||
} else {
|
||||
log('✗ API chrome non disponible (normal hors extension)', 'error');
|
||||
}
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
log('🧪 Début des tests...', 'info');
|
||||
|
||||
// Test file loading
|
||||
await Promise.all([
|
||||
testFile('sidepanel.html'),
|
||||
testFile('sidepanel.js'),
|
||||
testFile('content.js'),
|
||||
testFile('background.js'),
|
||||
testFile('manifest.json')
|
||||
]);
|
||||
|
||||
// Test sidepanel.js execution
|
||||
await testSidepanelJS();
|
||||
}
|
||||
|
||||
runTests();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -100,13 +100,19 @@ 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 => ({
|
||||
|
||||
// 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
|
||||
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)
|
||||
@@ -114,6 +120,40 @@ export class SemanticSearchService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Map<string, string[]>> {
|
||||
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<string, string[]>()
|
||||
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…).
|
||||
|
||||
@@ -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",
|
||||
|
||||
51
memento-note/scripts/migrate-chunk-embeddings.ts
Normal file
51
memento-note/scripts/migrate-chunk-embeddings.ts
Normal file
@@ -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)
|
||||
2
monitoring/metrics-token.example
Normal file
2
monitoring/metrics-token.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# Copy to metrics-token at deploy time (see scripts/deploy-prod.sh)
|
||||
# Example: openssl rand -hex 32 > monitoring/metrics-token
|
||||
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user