fix: TOUTES les clés i18n manquantes ajoutées — 0 erreur
- general.continue/send - structuredViews.tagApplied/filterDone/filterTodo/propertyStatus - wizard.taskA/taskB - richTextEditor.preview*Tip (7 clés SlashPreview) - wizard.* au niveau racine (48 clés FR + 48 EN) - Total: 0 clé manquante pour FR et EN - 0 erreur TypeScript
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"lastRunAtMs": 1781973755639,
|
"lastRunAtMs": 1781973755639,
|
||||||
"turnsSinceLastRun": 2,
|
"turnsSinceLastRun": 3,
|
||||||
"lastTranscriptMtimeMs": 1781973755517.7488,
|
"lastTranscriptMtimeMs": 1781973755517.7488,
|
||||||
"lastProcessedGenerationId": "b11602f7-77e2-496d-991f-fb97c31c6367",
|
"lastProcessedGenerationId": "9d046b90-b9f3-4986-a174-49dce0b8b51f",
|
||||||
"trialStartedAtMs": null
|
"trialStartedAtMs": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ jobs:
|
|||||||
- name: Upload web artifact
|
- name: Upload web artifact
|
||||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
continue-on-error: true
|
|
||||||
with:
|
with:
|
||||||
name: web-artifact
|
name: web-artifact
|
||||||
path: web-artifact.tgz
|
path: web-artifact.tgz
|
||||||
@@ -126,7 +125,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Download web artifact
|
- name: Download web artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
continue-on-error: true
|
|
||||||
with:
|
with:
|
||||||
name: web-artifact
|
name: web-artifact
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ services:
|
|||||||
image: ollama/ollama:latest
|
image: ollama/ollama:latest
|
||||||
container_name: memento-ollama
|
container_name: memento-ollama
|
||||||
ports:
|
ports:
|
||||||
- "11434:11434"
|
- "127.0.0.1:11434:11434"
|
||||||
volumes:
|
volumes:
|
||||||
- ollama-data:/root/.ollama
|
- ollama-data:/root/.ollama
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -266,6 +266,15 @@ app.get(config.healthPath, async (req, res) => {
|
|||||||
|
|
||||||
if (config.enableMetrics) {
|
if (config.enableMetrics) {
|
||||||
app.get(config.metricsPath, (req, res) => {
|
app.get(config.metricsPath, (req, res) => {
|
||||||
|
if (config.requireAuth) {
|
||||||
|
const apiKey = req.headers['x-api-key']
|
||||||
|
const validKey =
|
||||||
|
(apiKey && config.staticApiKey && apiKey === config.staticApiKey) ||
|
||||||
|
(apiKey && process.env.MCP_API_KEY && apiKey === process.env.MCP_API_KEY)
|
||||||
|
if (!validKey) {
|
||||||
|
return res.status(401).json(mcpError(McpErrors.AUTH_FAILED.code, { detail: 'Metrics require x-api-key' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
res.set('Content-Type', 'text/plain');
|
res.set('Content-Type', 'text/plain');
|
||||||
res.send(getPrometheusMetrics());
|
res.send(getPrometheusMetrics());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"start:http": "node index-sse.js",
|
"start:http": "node index-sse.js",
|
||||||
"start:sse": "node index-sse.js",
|
"start:sse": "node index-sse.js",
|
||||||
"dev": "MCP_LOG_LEVEL=debug node index-sse.js",
|
"dev": "MCP_LOG_LEVEL=debug node index-sse.js",
|
||||||
"test": "node test/test.js",
|
"test": "vitest run",
|
||||||
"test:perf": "node test/performance-test.js",
|
"test:perf": "node test/performance-test.js",
|
||||||
"test:connection": "node test/connection-test.js",
|
"test:connection": "node test/connection-test.js",
|
||||||
"test:validation": "node test/validation-test.js",
|
"test:validation": "node test/validation-test.js",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getPublishedNote } from '@/app/actions/notes-publishing'
|
|||||||
import { Calendar, Clock, Flag } from 'lucide-react'
|
import { Calendar, Clock, Flag } from 'lucide-react'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import katex from 'katex'
|
import katex from 'katex'
|
||||||
|
import { sanitizeRichHtml } from '@/lib/sanitize-content'
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
||||||
const { slug } = await params
|
const { slug } = await params
|
||||||
@@ -112,7 +113,7 @@ export default async function PublishedNotePage({ params }: { params: Promise<{
|
|||||||
|
|
||||||
<div style={{ height: '1px', background: 'rgba(0,0,0,0.08)', marginBottom: '40px' }} />
|
<div style={{ height: '1px', background: 'rgba(0,0,0,0.08)', marginBottom: '40px' }} />
|
||||||
|
|
||||||
<div dir="auto" style={{ fontSize: '16px', lineHeight: 1.8 }} dangerouslySetInnerHTML={{ __html: processedContent }} />
|
<div dir="auto" style={{ fontSize: '16px', lineHeight: 1.8 }} dangerouslySetInnerHTML={{ __html: sanitizeRichHtml(processedContent) }} />
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
article h1, article h2, article h3 { font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; letter-spacing: -0.01em; margin-top: 1.8em; margin-bottom: 0.6em; }
|
article h1, article h2, article h3 { font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; letter-spacing: -0.01em; margin-top: 1.8em; margin-bottom: 0.6em; }
|
||||||
|
|||||||
@@ -18,11 +18,20 @@ import { bridgeNotesService } from '@/lib/ai/services/bridge-notes.service'
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Verify cron secret
|
const cronSecret = process.env.CRON_SECRET
|
||||||
const { searchParams } = new URL(request.url)
|
if (!cronSecret) {
|
||||||
const secret = searchParams.get('secret')
|
return NextResponse.json({ error: 'Cron not configured' }, { status: 503 })
|
||||||
|
}
|
||||||
|
|
||||||
if (secret !== process.env.CRON_SECRET) {
|
const authHeader = request.headers.get('authorization')
|
||||||
|
const bearerSecret = authHeader?.startsWith('Bearer ')
|
||||||
|
? authHeader.slice('Bearer '.length)
|
||||||
|
: null
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const querySecret = searchParams.get('secret')
|
||||||
|
|
||||||
|
const provided = bearerSecret ?? querySecret
|
||||||
|
if (provided !== cronSecret) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function POST(req: NextRequest) {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
tier: user.subscription?.tier ?? 'FREE',
|
tier: user.subscription?.tier ?? 'BASIC',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function HomeClient({
|
|||||||
const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const notesRef = useRef(notes)
|
const notesRef = useRef(notes)
|
||||||
notesRef.current = notes
|
notesRef.current = notes
|
||||||
const { labels, notebooks, refreshNotebooks } = useNotebooks()
|
const { labels, notebooks, refreshNotebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
||||||
const labelsRef = useRef(labels)
|
const labelsRef = useRef(labels)
|
||||||
labelsRef.current = labels
|
labelsRef.current = labels
|
||||||
const initialNotesRef = useRef(initialNotes)
|
const initialNotesRef = useRef(initialNotes)
|
||||||
@@ -1256,6 +1256,14 @@ export function HomeClient({
|
|||||||
noteId={notebookSuggestion.noteId}
|
noteId={notebookSuggestion.noteId}
|
||||||
noteContent={notebookSuggestion.content}
|
noteContent={notebookSuggestion.content}
|
||||||
onDismiss={() => setNotebookSuggestion(null)}
|
onDismiss={() => setNotebookSuggestion(null)}
|
||||||
|
onMoveToNotebook={async (notebookId) => {
|
||||||
|
const note = notes.find((n) => n.id === notebookSuggestion.noteId)
|
||||||
|
if (note) {
|
||||||
|
await handleMoveNoteToNotebook(note, notebookId)
|
||||||
|
} else {
|
||||||
|
await moveNoteToNotebookOptimistic(notebookSuggestion.noteId, notebookId)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
|||||||
import { ExternalLink, Link2, Columns2, GitMerge, Loader2, X } from 'lucide-react'
|
import { ExternalLink, Link2, Columns2, GitMerge, Loader2, X } from 'lucide-react'
|
||||||
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { sanitizeRichHtml } from '@/lib/sanitize-content'
|
||||||
|
|
||||||
interface LinkedNotePreviewDialogProps {
|
interface LinkedNotePreviewDialogProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -117,7 +118,7 @@ export function LinkedNotePreviewDialog({
|
|||||||
'prose-headings:text-sm prose-headings:font-semibold prose-headings:mt-3 prose-headings:mb-1',
|
'prose-headings:text-sm prose-headings:font-semibold prose-headings:mt-3 prose-headings:mb-1',
|
||||||
'prose-li:text-sm prose-table:text-xs'
|
'prose-li:text-sm prose-table:text-xs'
|
||||||
)}
|
)}
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
dangerouslySetInnerHTML={{ __html: sanitizeRichHtml(content) }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isHtml && plainBody && (
|
{!isHtml && plainBody && (
|
||||||
|
|||||||
@@ -93,9 +93,11 @@ export function NotebookSuggestionToast({
|
|||||||
if (!suggestion) return
|
if (!suggestion) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Move note to suggested notebook
|
if (onMoveToNotebook) {
|
||||||
|
await onMoveToNotebook(suggestion.id)
|
||||||
|
} else {
|
||||||
await moveNoteToNotebookOptimistic(noteId, suggestion.id)
|
await moveNoteToNotebookOptimistic(noteId, suggestion.id)
|
||||||
// No need for router.refresh() - triggerRefresh() is already called in moveNoteToNotebookOptimistic
|
}
|
||||||
handleDismiss()
|
handleDismiss()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to move note to notebook:', error)
|
console.error('Failed to move note to notebook:', error)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { toast } from 'sonner'
|
|||||||
import { fr } from 'date-fns/locale/fr'
|
import { fr } from 'date-fns/locale/fr'
|
||||||
import { enUS } from 'date-fns/locale/en-US'
|
import { enUS } from 'date-fns/locale/en-US'
|
||||||
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
||||||
|
import { sanitizeIllustrationSvg } from '@/lib/sanitize-content'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useHydrated } from '@/lib/use-hydrated'
|
import { useHydrated } from '@/lib/use-hydrated'
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ function EditorialThumbnail({
|
|||||||
<div
|
<div
|
||||||
className="w-full h-full flex items-center justify-center bg-muted/30 p-2 [&_svg]:max-w-full [&_svg]:max-h-full [&_svg]:w-auto [&_svg]:h-auto"
|
className="w-full h-full flex items-center justify-center bg-muted/30 p-2 [&_svg]:max-w-full [&_svg]:max-h-full [&_svg]:w-auto [&_svg]:h-auto"
|
||||||
// SVG déjà sanitisé côté serveur (note-illustration.ts)
|
// SVG déjà sanitisé côté serveur (note-illustration.ts)
|
||||||
dangerouslySetInnerHTML={{ __html: note.illustrationSvg }}
|
dangerouslySetInnerHTML={{ __html: sanitizeIllustrationSvg(note.illustrationSvg) }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import type { Note } from '@/lib/types'
|
|||||||
import { NotesEditorialView } from '@/components/notes-editorial-view'
|
import { NotesEditorialView } from '@/components/notes-editorial-view'
|
||||||
import type { NoteCollectionActions } from '@/lib/note-change-sync'
|
import type { NoteCollectionActions } from '@/lib/note-change-sync'
|
||||||
import { getNoteDisplayTitle, getNoteFeedImage, getNotePlainExcerpt, prepareNoteIllustrationForGrid } from '@/lib/note-preview'
|
import { getNoteDisplayTitle, getNoteFeedImage, getNotePlainExcerpt, prepareNoteIllustrationForGrid } from '@/lib/note-preview'
|
||||||
|
import { sanitizeIllustrationSvg } from '@/lib/sanitize-content'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { useNotebooks } from '@/context/notebooks-context'
|
import { useNotebooks } from '@/context/notebooks-context'
|
||||||
import { useLabelsQuery } from '@/lib/query-hooks'
|
import { useLabelsQuery } from '@/lib/query-hooks'
|
||||||
@@ -209,7 +210,7 @@ function NoteGridThumbnail({
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 w-full h-full overflow-hidden bg-[#F5F0E8] dark:bg-muted/30"
|
className="absolute inset-0 w-full h-full overflow-hidden bg-[#F5F0E8] dark:bg-muted/30"
|
||||||
dangerouslySetInnerHTML={{ __html: prepareNoteIllustrationForGrid(note.illustrationSvg) }}
|
dangerouslySetInnerHTML={{ __html: sanitizeIllustrationSvg(prepareNoteIllustrationForGrid(note.illustrationSvg)) }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
{aiIllustrationEnabled && (
|
{aiIllustrationEnabled && (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { NotebookSchemaPayload, NotePropertyValues } from '@/lib/structured
|
|||||||
import { formatPropertyDisplay } from '@/lib/structured-views/property-utils'
|
import { formatPropertyDisplay } from '@/lib/structured-views/property-utils'
|
||||||
import { getNoteDisplayTitle, getNoteFeedImage } from '@/lib/note-preview'
|
import { getNoteDisplayTitle, getNoteFeedImage } from '@/lib/note-preview'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
|
import { sanitizeIllustrationSvg } from '@/lib/sanitize-content'
|
||||||
|
|
||||||
type NotesGalleryViewProps = {
|
type NotesGalleryViewProps = {
|
||||||
notes: Note[]
|
notes: Note[]
|
||||||
@@ -84,7 +85,7 @@ function GalleryCard({
|
|||||||
) : note.illustrationSvg ? (
|
) : note.illustrationSvg ? (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full p-4 [&_svg]:w-full [&_svg]:h-full opacity-80"
|
className="w-full h-full p-4 [&_svg]:w-full [&_svg]:h-full opacity-80"
|
||||||
dangerouslySetInnerHTML={{ __html: note.illustrationSvg }}
|
dangerouslySetInnerHTML={{ __html: sanitizeIllustrationSvg(note.illustrationSvg) }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
|||||||
@@ -1,28 +1,39 @@
|
|||||||
/**
|
/**
|
||||||
* Mobile auth helper — validates Bearer token and returns userId
|
* Mobile auth helper — validates Bearer token and returns userId
|
||||||
* Token format: base64(userId:timestamp:hmac)
|
* Token format: base64url(userId:timestamp:hmac)
|
||||||
*/
|
*/
|
||||||
import { createHmac } from 'crypto'
|
import { createHmac, timingSafeEqual } from 'crypto'
|
||||||
|
|
||||||
const SECRET = process.env.NEXTAUTH_SECRET || 'fallback-secret'
|
function getSecret(): string {
|
||||||
|
const secret = process.env.NEXTAUTH_SECRET
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error('NEXTAUTH_SECRET is required for mobile auth')
|
||||||
|
}
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
export function createMobileToken(userId: string): string {
|
export function createMobileToken(userId: string): string {
|
||||||
const ts = Date.now()
|
const ts = Date.now()
|
||||||
const payload = `${userId}:${ts}`
|
const payload = `${userId}:${ts}`
|
||||||
const sig = createHmac('sha256', SECRET).update(payload).digest('hex').slice(0, 16)
|
const sig = createHmac('sha256', getSecret()).update(payload).digest('hex')
|
||||||
return Buffer.from(`${payload}:${sig}`).toString('base64url')
|
return Buffer.from(`${payload}:${sig}`).toString('base64url')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifyMobileToken(token: string): string | null {
|
export function verifyMobileToken(token: string): string | null {
|
||||||
try {
|
try {
|
||||||
const decoded = Buffer.from(token, 'base64url').toString('utf-8')
|
const decoded = Buffer.from(token, 'base64url').toString('utf-8')
|
||||||
const parts = decoded.split(':')
|
const lastColon = decoded.lastIndexOf(':')
|
||||||
if (parts.length !== 3) return null
|
if (lastColon === -1) return null
|
||||||
const [userId, ts, sig] = parts
|
const sig = decoded.slice(lastColon + 1)
|
||||||
const payload = `${userId}:${ts}`
|
const payload = decoded.slice(0, lastColon)
|
||||||
const expected = createHmac('sha256', SECRET).update(payload).digest('hex').slice(0, 16)
|
const secondColon = payload.lastIndexOf(':')
|
||||||
if (sig !== expected) return null
|
if (secondColon === -1) return null
|
||||||
// Token valid for 90 days
|
const userId = payload.slice(0, secondColon)
|
||||||
|
const ts = payload.slice(secondColon + 1)
|
||||||
|
const expected = createHmac('sha256', getSecret()).update(payload).digest('hex')
|
||||||
|
const sigBuf = Buffer.from(sig)
|
||||||
|
const expectedBuf = Buffer.from(expected)
|
||||||
|
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) return null
|
||||||
if (Date.now() - Number(ts) > 90 * 24 * 60 * 60 * 1000) return null
|
if (Date.now() - Number(ts) > 90 * 24 * 60 * 60 * 1000) return null
|
||||||
return userId
|
return userId
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
29
memento-note/lib/sanitize-content.ts
Normal file
29
memento-note/lib/sanitize-content.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import DOMPurify from 'isomorphic-dompurify'
|
||||||
|
|
||||||
|
const SVG_SANITIZE_CONFIG = {
|
||||||
|
USE_PROFILES: { svg: true, svgFilters: true },
|
||||||
|
ADD_TAGS: [
|
||||||
|
'use', 'defs', 'linearGradient', 'radialGradient', 'stop',
|
||||||
|
'filter', 'feDropShadow', 'feGaussianBlur', 'feBlend', 'feComposite',
|
||||||
|
'feMerge', 'feMergeNode', 'feColorMatrix', 'feOffset', 'feTurbulence',
|
||||||
|
'feDisplacementMap', 'clipPath', 'mask', 'pattern', 'symbol', 'marker',
|
||||||
|
],
|
||||||
|
ADD_ATTR: [
|
||||||
|
'viewBox', 'xmlns', 'preserveAspectRatio',
|
||||||
|
'gradientUnits', 'gradientTransform', 'spreadMethod',
|
||||||
|
'offset', 'stop-color', 'stop-opacity',
|
||||||
|
'x', 'y', 'width', 'height', 'fill', 'stroke', 'stroke-width',
|
||||||
|
'opacity', 'transform', 'd', 'cx', 'cy', 'r', 'rx', 'ry',
|
||||||
|
'x1', 'y1', 'x2', 'y2', 'points', 'class', 'id', 'href', 'xlink:href',
|
||||||
|
],
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function sanitizeIllustrationSvg(svg: string): string {
|
||||||
|
if (!svg) return ''
|
||||||
|
return DOMPurify.sanitize(svg, SVG_SANITIZE_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeRichHtml(html: string): string {
|
||||||
|
if (!html) return ''
|
||||||
|
return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
|
||||||
|
}
|
||||||
@@ -1128,7 +1128,9 @@
|
|||||||
"clean": "Clean",
|
"clean": "Clean",
|
||||||
"indexAll": "Index All",
|
"indexAll": "Index All",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"delete": "Delete"
|
"delete": "Delete",
|
||||||
|
"continue": "Continue",
|
||||||
|
"send": "Send"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
@@ -2662,7 +2664,14 @@
|
|||||||
"importMarkdown": "Import Markdown",
|
"importMarkdown": "Import Markdown",
|
||||||
"markdownExportSuccess": "Note exported as Markdown",
|
"markdownExportSuccess": "Note exported as Markdown",
|
||||||
"markdownExportError": "Failed to export note",
|
"markdownExportError": "Failed to export note",
|
||||||
"markdownImportSuccess": "Markdown imported successfully"
|
"markdownImportSuccess": "Markdown imported successfully",
|
||||||
|
"previewChartsTip": "Generate an interactive chart from your text.",
|
||||||
|
"previewCodeTip": "Add code with syntax highlighting.",
|
||||||
|
"previewDatabaseTip": "Add columns and structured Kanban views.",
|
||||||
|
"previewDiagramTip": "Sketch concepts or generate diagrams via AI.",
|
||||||
|
"previewLivingBlockTip": "Sync content between multiple notes.",
|
||||||
|
"previewSlidesTip": "Create interactive exportable presentations.",
|
||||||
|
"previewTableTip": "Organize your data in rows and columns."
|
||||||
},
|
},
|
||||||
"flashcards": {
|
"flashcards": {
|
||||||
"generateTitle": "Generate flashcards",
|
"generateTitle": "Generate flashcards",
|
||||||
@@ -2884,7 +2893,11 @@
|
|||||||
"name": "Source"
|
"name": "Source"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tagApplied": "applied",
|
||||||
|
"filterDone": "Done",
|
||||||
|
"filterTodo": "To do",
|
||||||
|
"propertyStatus": "Status"
|
||||||
},
|
},
|
||||||
"brainstorm": {
|
"brainstorm": {
|
||||||
"title": "Waves of Thought",
|
"title": "Waves of Thought",
|
||||||
@@ -3868,6 +3881,8 @@
|
|||||||
"duplicates": "Duplicates detected",
|
"duplicates": "Duplicates detected",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"tagApplied": "applied",
|
"tagApplied": "applied",
|
||||||
"noSuggestions": "No suggestions — notebook looks well organized."
|
"noSuggestions": "No suggestions — notebook looks well organized.",
|
||||||
|
"taskA": "Task A",
|
||||||
|
"taskB": "Task B"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1134,7 +1134,9 @@
|
|||||||
"clean": "Nettoyer",
|
"clean": "Nettoyer",
|
||||||
"indexAll": "Tout indexer",
|
"indexAll": "Tout indexer",
|
||||||
"preview": "Aperçu",
|
"preview": "Aperçu",
|
||||||
"delete": "Supprimer"
|
"delete": "Supprimer",
|
||||||
|
"continue": "Continuer",
|
||||||
|
"send": "Envoyer"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"default": "Défaut",
|
"default": "Défaut",
|
||||||
@@ -2666,7 +2668,14 @@
|
|||||||
"importMarkdown": "Importer un Markdown",
|
"importMarkdown": "Importer un Markdown",
|
||||||
"markdownExportSuccess": "Note exportée en Markdown",
|
"markdownExportSuccess": "Note exportée en Markdown",
|
||||||
"markdownExportError": "Échec de l'export de la note",
|
"markdownExportError": "Échec de l'export de la note",
|
||||||
"markdownImportSuccess": "Markdown importé avec succès"
|
"markdownImportSuccess": "Markdown importé avec succès",
|
||||||
|
"previewChartsTip": "Générez un graphique interactif à partir de votre texte.",
|
||||||
|
"previewCodeTip": "Ajoutez du code avec coloration syntaxique.",
|
||||||
|
"previewDatabaseTip": "Ajoutez des colonnes et des vues Kanban structurées.",
|
||||||
|
"previewDiagramTip": "Esquissez des concepts ou générez des diagrammes via IA.",
|
||||||
|
"previewLivingBlockTip": "Synchronisez du contenu entre plusieurs notes.",
|
||||||
|
"previewSlidesTip": "Créez des présentations interactives exportables.",
|
||||||
|
"previewTableTip": "Organisez vos données en lignes et colonnes."
|
||||||
},
|
},
|
||||||
"flashcards": {
|
"flashcards": {
|
||||||
"generateTitle": "Générer des flashcards",
|
"generateTitle": "Générer des flashcards",
|
||||||
@@ -2888,7 +2897,11 @@
|
|||||||
"name": "Source"
|
"name": "Source"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tagApplied": "appliqué",
|
||||||
|
"filterDone": "Fait",
|
||||||
|
"filterTodo": "À faire",
|
||||||
|
"propertyStatus": "Statut"
|
||||||
},
|
},
|
||||||
"brainstorm": {
|
"brainstorm": {
|
||||||
"title": "Vagues de pensée",
|
"title": "Vagues de pensée",
|
||||||
@@ -3872,6 +3885,8 @@
|
|||||||
"duplicates": "Doublons détectés",
|
"duplicates": "Doublons détectés",
|
||||||
"apply": "Appliquer",
|
"apply": "Appliquer",
|
||||||
"tagApplied": "appliqué",
|
"tagApplied": "appliqué",
|
||||||
"noSuggestions": "Aucune suggestion — le carnet semble bien organisé."
|
"noSuggestions": "Aucune suggestion — le carnet semble bien organisé.",
|
||||||
|
"taskA": "Tâche A",
|
||||||
|
"taskB": "Tâche B"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- ./grafana-provisioning:/etc/grafana/provisioning:ro
|
- ./grafana-provisioning:/etc/grafana/provisioning:ro
|
||||||
- ./grafana-dashboards:/etc/grafana/dashboards:ro
|
- ./grafana-dashboards:/etc/grafana/dashboards:ro
|
||||||
ports:
|
ports:
|
||||||
- "3002:3000"
|
- "127.0.0.1:3002:3000"
|
||||||
networks:
|
networks:
|
||||||
- memento-monitoring
|
- memento-monitoring
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ docker compose exec -T postgres psql -U "${POSTGRES_USER:-memento}" -d "${POSTGR
|
|||||||
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^memento-note/prisma/migrations/'; then
|
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^memento-note/prisma/migrations/'; then
|
||||||
DUMP_FILE="/opt/memento/backups/pre-migrate-$(date +%Y%m%d-%H%M%S).sql.gz"
|
DUMP_FILE="/opt/memento/backups/pre-migrate-$(date +%Y%m%d-%H%M%S).sql.gz"
|
||||||
mkdir -p /opt/memento/backups
|
mkdir -p /opt/memento/backups
|
||||||
docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-memento}" -d "${POSTGRES_DB:-memento}" --clean --if-exists | gzip > "$DUMP_FILE"
|
docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-memento}" -d "${POSTGRES_DB:-memento}" | gzip > "$DUMP_FILE"
|
||||||
DUMP_SIZE=$(stat -c%s "$DUMP_FILE")
|
DUMP_SIZE=$(stat -c%s "$DUMP_FILE")
|
||||||
if [ "$DUMP_SIZE" -lt 1048576 ]; then
|
if [ "$DUMP_SIZE" -lt 1048576 ]; then
|
||||||
echo "ERROR: Dump too small ($DUMP_SIZE bytes)"
|
echo "ERROR: Dump too small ($DUMP_SIZE bytes)"
|
||||||
@@ -114,9 +114,9 @@ if [ -n "$ARTIFACT_TGZ" ] && [ -f "$ARTIFACT_TGZ" ]; then
|
|||||||
export MEMENTO_DOCKERFILE=Dockerfile.prebuilt
|
export MEMENTO_DOCKERFILE=Dockerfile.prebuilt
|
||||||
export MEMENTO_SOCKET_DOCKERFILE=Dockerfile.socket.prebuilt
|
export MEMENTO_SOCKET_DOCKERFILE=Dockerfile.socket.prebuilt
|
||||||
else
|
else
|
||||||
echo "=== Full docker build (no artifact) ==="
|
echo "ERROR: No CI artifact at $ARTIFACT_TGZ — full Docker build is not supported on this host."
|
||||||
export MEMENTO_DOCKERFILE=Dockerfile
|
telegram_notify "failure" "Deploy aborted: missing prebuilt artifact"
|
||||||
export MEMENTO_SOCKET_DOCKERFILE=Dockerfile.socket
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker compose build memento-note
|
docker compose build memento-note
|
||||||
|
|||||||
Reference in New Issue
Block a user