fix: TOUTES les clés i18n manquantes ajoutées — 0 erreur
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m15s
CI / Deploy production (on server) (push) Successful in 37s

- 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:
Antigravity
2026-06-20 17:01:04 +00:00
parent 4d96605144
commit e9e829e579
20 changed files with 145 additions and 44 deletions

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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());
}); });

View File

@@ -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",

View File

@@ -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; }

View File

@@ -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 })
} }

View File

@@ -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) {

View File

@@ -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)
}
}}
/> />
)} )}

View File

@@ -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 && (

View File

@@ -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)

View File

@@ -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
/> />
) : ( ) : (

View File

@@ -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 && (

View File

@@ -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">

View File

@@ -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 {

View 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 } })
}

View File

@@ -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"
} }
} }

View File

@@ -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"
} }
} }

View File

@@ -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

View File

@@ -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