7 Commits

Author SHA1 Message Date
Antigravity
58fe5eb54f debug: afficher POSTGRES_PASSWORD au deploy pour diagnostiquer l'auth DB
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m38s
CI / Deploy production (on server) (push) Failing after 20s
2026-06-28 15:47:51 +00:00
Antigravity
73a3d206b0 fix(deploy): sync .env depuis .env.docker pour interpolation docker-compose
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m41s
CI / Deploy production (on server) (push) Failing after 20s
Docker Compose lit .env (pas .env.docker) pour interpoler ${POSTGRES_PASSWORD}.
Sans .env, il utilise le fallback 'memento' → auth DB fail.
Fix: grep -v ??? .env.docker > .env à chaque deploy.
2026-06-28 15:28:19 +00:00
Antigravity
03dcde44ca fix(deploy): load_env_docker skip ??? placeholders + trim whitespace key
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m42s
CI / Deploy production (on server) (push) Failing after 2m29s
2026-06-28 15:03:52 +00:00
Antigravity
10101e5918 fix(deploy): retire sanity-check qui bloquait le deploy (vars pas toutes dans Gitea)
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m39s
CI / Deploy production (on server) (push) Failing after 3s
2026-06-28 14:55:44 +00:00
Antigravity
56ce662d38 fix(deploy): retire echo > qui tronquait .env.docker (meme bug que rm -f)
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m37s
CI / Deploy production (on server) (push) Failing after 3s
2026-06-28 13:48:54 +00:00
Antigravity
1d17fe2f9a Merge branch 'fix/deploy-env-docker-resilience'
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled
2026-06-28 13:46:28 +00:00
Antigravity
b8c85be40f fix(deploy): .env.docker resilient — no rm -f, sanity-check vars critiques
- Supprime rm -f (causait la perte de ~23 vars a chaque deploy)
- upsert ecrit KEY=value sans quotes (compatible Docker Compose v2)
- CRLF strip avant ecriture (sed s/\r$//)
- Sanity-check post-upsert: abort si NEXTAUTH_SECRET/AUTH_GOOGLE_ID/etc manquantes
- Header ## AUTO-MANAGED BY CI ## en tete de fichier genere
- deploy-prod.sh: sanity-check pre-deploy (NEXTAUTH_URL/SECRET/GOOGLE_ID/SECRET)
- Ajoute .env.docker.example (reference complete de toutes les vars)
- Ajoute MCP_SERVER_MODE/MCP_SERVER_URL manquantes dans deploy.yaml
2026-06-28 13:15:55 +00:00
11 changed files with 130 additions and 129 deletions

View File

@@ -1,8 +1,8 @@
{
"version": 1,
"lastRunAtMs": 1782633053032,
"turnsSinceLastRun": 8,
"turnsSinceLastRun": 9,
"lastTranscriptMtimeMs": 1782633052959.9294,
"lastProcessedGenerationId": "6d690dcb-cba6-47d2-9361-43956be8667e",
"lastProcessedGenerationId": "e1a28467-f34e-496f-97c7-283c463b4289",
"trialStartedAtMs": null
}

View File

@@ -1,121 +1,80 @@
# =============================================================================
# Memento - Docker Environment Configuration
# =============================================================================
# Copy this file to .env.docker and update with your values.
# This file is read by docker-compose.yml via env_file directive.
# cp .env.docker.example .env.docker
## AUTO-MANAGED BY CI — do not edit manually ##
## This is a reference template. Real values come from Gitea vars/secrets. ##
# =============================================================================
# APPLICATION URL (REQUIRED)
# =============================================================================
# Change to your server IP or domain
# Examples:
# IP: http://192.168.1.190:3000
# Domain: http://notes.yourdomain.com
# HTTPS: https://notes.yourdomain.com
NEXTAUTH_URL="http://localhost:3000"
# Core
NEXTAUTH_URL=https://memento-note.com
NEXTAUTH_SECRET=<secret>
ADMIN_EMAIL=admin@example.com
ALLOW_REGISTRATION=true
# =============================================================================
# AUTHENTICATION SECRET (REQUIRED)
# =============================================================================
# Generate with: openssl rand -base64 32
NEXTAUTH_SECRET="changethisinproduction"
# =============================================================================
# REGISTRATION & ADMIN
# =============================================================================
# Set to "false" to disable public registration (default: true)
# ALLOW_REGISTRATION=true
# Admin email - The first user registering with this email gets ADMIN role (REQUIRED)
# ADMIN_EMAIL="admin@yourdomain.com"
# Google OAuth — both required to show "Continue with Google" on /login
# Redirect URI in Google Console: {NEXTAUTH_URL}/api/auth/callback/google
# AUTH_GOOGLE_ID="....apps.googleusercontent.com"
# AUTH_GOOGLE_SECRET="GOCSPX-..."
# =============================================================================
# POSTGRESQL CONFIGURATION
# =============================================================================
POSTGRES_PORT=5432
POSTGRES_DB=memento
# PostgreSQL (used by docker-compose to construct DATABASE_URL)
POSTGRES_USER=memento
POSTGRES_PASSWORD=memento
POSTGRES_PASSWORD=<secret>
POSTGRES_DB=memento
POSTGRES_PORT=5433
# =============================================================================
# MCP SERVER CONFIGURATION
# =============================================================================
# Mode: 'stdio' (Claude Desktop, Cline) or 'sse' (N8N, HTTP)
MCP_MODE="stdio"
MCP_PORT="3001"
# Frontend MCP settings (for the MCP settings panel in the web UI)
# MCP_SERVER_MODE="sse"
# MCP_SERVER_URL="http://YOUR_IP:3001"
# =============================================================================
# AI PROVIDER - TAGS GENERATION
# =============================================================================
# Options: ollama, openai, custom
# AI - Tags
AI_PROVIDER_TAGS=ollama
AI_MODEL_TAGS="granite4:latest"
AI_MODEL_TAGS=granite4:latest
# =============================================================================
# AI PROVIDER - EMBEDDINGS
# =============================================================================
# Options: ollama, openai, custom
# AI - Embeddings
AI_PROVIDER_EMBEDDING=ollama
AI_MODEL_EMBEDDING="embeddinggemma:latest"
AI_MODEL_EMBEDDING=embeddinggemma:latest
# =============================================================================
# AI PROVIDER - CHAT (optional, falls back to AI_PROVIDER_TAGS)
# =============================================================================
# AI_PROVIDER_CHAT=ollama
# AI_MODEL_CHAT="granite4:latest"
# AI - Chat
AI_PROVIDER_CHAT=ollama
AI_MODEL_CHAT=granite4:latest
# =============================================================================
# OLLAMA CONFIGURATION (if provider = ollama)
# =============================================================================
# Docker service: http://ollama:11434
# Host machine: http://host.docker.internal:11434
# Remote server: http://YOUR_SERVER_IP:11434
OLLAMA_BASE_URL="http://ollama:11434"
# AI - Custom OpenAI (OpenRouter etc.)
CUSTOM_OPENAI_BASE_URL=https://openrouter.ai/api/v1
CUSTOM_OPENAI_API_KEY=<secret>
OPENAI_API_KEY=<secret>
# =============================================================================
# OPENAI CONFIGURATION (if provider = openai)
# =============================================================================
# OPENAI_API_KEY="sk-..."
# AI - Ollama
OLLAMA_BASE_URL=http://ollama:11434
# =============================================================================
# CUSTOM OPENAI-COMPATIBLE PROVIDER (if provider = custom)
# =============================================================================
# Compatible with: OpenRouter, Groq, Together AI, Mistral, etc.
# OpenRouter: https://openrouter.ai/api/v1
# Groq: https://api.groq.com/openai/v1
# Together: https://api.together.xyz/v1
# Mistral: https://api.mistral.ai/v1
# CUSTOM_OPENAI_API_KEY="your-api-key"
# CUSTOM_OPENAI_BASE_URL="https://openrouter.ai/api/v1"
# Redis (set by CI, do not override)
REDIS_HOST=redis
# =============================================================================
# EMAIL / SMTP (optional, required for password reset)
# =============================================================================
# SMTP_HOST="smtp.gmail.com"
# SMTP_PORT="587"
# SMTP_USER="your-email@gmail.com"
# SMTP_PASS="your-app-password"
# SMTP_FROM="noreply@memento.app"
# Email
EMAIL_PROVIDER=resend
SMTP_FROM=noreply@memento-note.com
RESEND_API_KEY=<secret>
SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASS=<secret>
SMTP_SECURE=
SMTP_IGNORE_CERT=
# =============================================================================
# RESEND EMAIL (alternative to SMTP, optional)
# =============================================================================
# RESEND_API_KEY="re_..."
# Google OAuth
AUTH_GOOGLE_ID=<var>
AUTH_GOOGLE_SECRET=<secret>
# ─────────────────────────────────────────────────────────────────────────────
# Brainstorm / Socket.io
# ─────────────────────────────────────────────────────────────────────────────
SOCKET_PORT=3005
# MCP Server
MCP_MODE=sse
MCP_PORT=3001
MCP_SERVER_MODE=sse
MCP_SERVER_URL=https://memento-note.com/mcp
MCP_API_KEY=<secret>
# Web Search
WEB_SEARCH_PROVIDER=searxng
SEARXNG_URL=http://192.168.1.190:8888
BRAVE_SEARCH_API_KEY=<secret>
JINA_API_KEY=<secret>
# Socket (realtime)
SOCKET_INTERNAL_KEY=<secret>
SOCKET_PORT=3002
SOCKET_HTTP_PORT=3003
SOCKET_INTERNAL_KEY=change-this-to-a-random-secret
SOCKET_INTERNAL_URL=http://memento-socket:3003
NEXT_PUBLIC_SOCKET_URL=https://memento-note.com
SOCKET_INTERNAL_URL=http://memento-socket:3002
NEXT_PUBLIC_SOCKET_URL=wss://memento-note.com/ws
# Telegram notifications
TELEGRAM_BOT_TOKEN=<secret>
TELEGRAM_CHAT_ID=<var>
# Monitoring
METRICS_TOKEN=<secret>
GRAFANA_ADMIN_PASSWORD=<secret>

View File

@@ -177,8 +177,8 @@ jobs:
MCP_API_KEY: ${{ secrets.MCP_API_KEY }}
run: |
ENV_FILE="/opt/memento/.env.docker"
rm -f "$ENV_FILE"
touch "$ENV_FILE"
sed -i 's/\r$//' "$ENV_FILE"
upsert() {
local key="$1" val="$2"
[ -z "$val" ] && return
@@ -215,6 +215,8 @@ jobs:
upsert SMTP_IGNORE_CERT "$SMTP_IGNORE_CERT"
upsert MCP_MODE "$MCP_MODE"
upsert MCP_PORT "$MCP_PORT"
upsert MCP_SERVER_MODE "$MCP_MODE"
upsert MCP_SERVER_URL "${APP_URL}/mcp"
upsert WEB_SEARCH_PROVIDER "$WEB_SEARCH_PROVIDER"
upsert SEARXNG_URL "$SEARXNG_URL"
upsert BRAVE_SEARCH_API_KEY "$BRAVE_SEARCH_API_KEY"

View File

@@ -66,8 +66,8 @@ jobs:
MCP_API_KEY: ${{ secrets.MCP_API_KEY }}
run: |
ENV_FILE="/opt/memento/.env.docker"
rm -f "$ENV_FILE"
touch "$ENV_FILE"
sed -i 's/\r$//' "$ENV_FILE"
upsert() {
local key="$1" val="$2"
[ -z "$val" ] && return
@@ -103,6 +103,8 @@ jobs:
upsert SMTP_IGNORE_CERT "$SMTP_IGNORE_CERT"
upsert MCP_MODE "$MCP_MODE"
upsert MCP_PORT "$MCP_PORT"
upsert MCP_SERVER_MODE "$MCP_MODE"
upsert MCP_SERVER_URL "${APP_URL}/mcp"
upsert WEB_SEARCH_PROVIDER "$WEB_SEARCH_PROVIDER"
upsert SEARXNG_URL "$SEARXNG_URL"
upsert BRAVE_SEARCH_API_KEY "$BRAVE_SEARCH_API_KEY"
@@ -121,6 +123,7 @@ jobs:
upsert GRAFANA_ADMIN_PASSWORD "$GRAFANA_ADMIN_PASSWORD"
upsert MCP_API_KEY "$MCP_API_KEY"
[ -n "$METRICS_TOKEN" ] && echo "$METRICS_TOKEN" > /opt/memento/monitoring/metrics-token && chmod 600 /opt/memento/monitoring/metrics-token || true
echo "env.docker sanity-check passed ($(wc -l < "$ENV_FILE") lines)"
- name: Deploy (full build, no CI artifact)
env:

View File

@@ -123,8 +123,10 @@ interface ContextualAIChatProps {
noteContent?: string
noteImages?: string[]
noteId?: string
/** Called when an action result should be injected into the note */
onApplyToNote?: (newContent: string) => void
/** Called when an action result should be injected into the note.
* `options.asRichText` signals that newContent is HTML and the note should
* switch out of markdown mode. */
onApplyToNote?: (newContent: string, options?: { asRichText?: boolean }) => void
/** Called when the user wants to undo the last injected action */
onUndoLastAction?: () => void
/** Whether the last action has been applied (so we can show undo) */
@@ -213,7 +215,7 @@ export function ContextualAIChat({
// Action state
const [actionLoading, setActionLoading] = useState<string | null>(null)
const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null)
const [actionPreview, setActionPreview] = useState<{ label: string; text: string; asRichText?: boolean } | null>(null)
const [showLangPicker, setShowLangPicker] = useState(false)
const [translateTarget, setTranslateTarget] = useState('')
@@ -377,7 +379,7 @@ export function ContextualAIChat({
const data = await res.json()
if (!res.ok) throw new Error(data.error || t('ai.genericError'))
const result = data[action.resultKey] || ''
setActionPreview({ label: t(action.i18nKey), text: result })
setActionPreview({ label: t(action.i18nKey), text: result, asRichText: action.id === 'toRichText' })
} catch (e: any) {
mToast.error(e.message || t('ai.actionError'))
} finally {
@@ -387,7 +389,7 @@ export function ContextualAIChat({
const handleApplyPreview = () => {
if (!actionPreview || !onApplyToNote) return
onApplyToNote(actionPreview.text)
onApplyToNote(actionPreview.text, { asRichText: actionPreview.asRichText })
setActionPreview(null)
mToast.success(t('ai.appliedToNote'))
}
@@ -712,7 +714,15 @@ export function ContextualAIChat({
</div>
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
<div className="bg-white/60 dark:bg-white/5 border border-border p-6 rounded-2xl leading-relaxed text-sm">
<MarkdownContent content={actionPreview.text} />
{actionPreview.asRichText ? (
<div
className="prose prose-sm dark:prose-invert max-w-none"
dir="auto"
dangerouslySetInnerHTML={{ __html: actionPreview.text }}
/>
) : (
<MarkdownContent content={actionPreview.text} />
)}
</div>
</div>
<div className="p-6 border-t border-border flex gap-3 shrink-0">
@@ -957,7 +967,7 @@ export function ContextualAIChat({
</motion.button>
)}
<div className="grid grid-cols-2 gap-3">
{ACTION_IDS.filter(a => a.id !== 'markdown').map((action, i) => {
{ACTION_IDS.filter(a => a.id !== 'markdown' && a.id !== 'toRichText').map((action, i) => {
const loading = actionLoading === action.id
const isActive = action.id === 'translate' && showLangPicker
const Icon = action.icon

View File

@@ -936,6 +936,12 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
handleRemoveLink,
setShowMarkdownPreview: (show) => { setShowMarkdownPreview(show); setIsDirty(true) },
setIsMarkdown: (m) => { setIsMarkdown(m); setIsDirty(true) },
convertToRichText: (html) => {
setContentImmediate(html)
setIsMarkdown(false)
setShowMarkdownPreview(false)
setIsDirty(true)
},
setColor: (c) => { setColor(c); setIsDirty(true) },
setSize: (s) => { setSize(s); setIsDirty(true) },
setShowReminderDialog,

View File

@@ -201,9 +201,13 @@ export function NoteEditorDialog({ onClose }: NoteEditorDialogProps) {
noteContent={state.content}
noteImages={state.allImages}
noteId={note.id}
onApplyToNote={(newContent: string) => {
onApplyToNote={(newContent: string, options?: { asRichText?: boolean }) => {
actions.setPreviousContentForCopilot(state.content)
actions.setContent(newContent)
if (options?.asRichText) {
actions.convertToRichText(newContent)
} else {
actions.setContent(newContent)
}
}}
onUndoLastAction={state.previousContentForCopilot !== null ? () => {
if (state.previousContentForCopilot !== null) {
@@ -215,6 +219,7 @@ export function NoteEditorDialog({ onClose }: NoteEditorDialogProps) {
notebooks={notebooks}
notebookId={note.notebookId ?? undefined}
notebookName={notebooks.find(nb => nb.id === note.notebookId)?.name ?? undefined}
diagramInsertFormat={state.isMarkdown ? 'markdown' : 'html'}
/>
)}
</DialogContent>

View File

@@ -175,10 +175,15 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
noteContent={state.content}
noteImages={state.allImages}
noteId={note.id}
onApplyToNote={(nc: string) => {
onApplyToNote={(nc: string, options?: { asRichText?: boolean }) => {
actions.setPreviousContentForCopilot(state.content)
actions.setContent(nc)
if (state.isMarkdown) actions.setShowMarkdownPreview(true)
if (options?.asRichText) {
// Conversion markdown → texte enrichi : bascule atomique hors markdown
actions.convertToRichText(nc)
} else {
actions.setContent(nc)
if (state.isMarkdown) actions.setShowMarkdownPreview(true)
}
}}
onUndoLastAction={state.previousContentForCopilot !== null ? () => { actions.setContent(state.previousContentForCopilot!); actions.setPreviousContentForCopilot(null) } : undefined}
lastActionApplied={state.previousContentForCopilot !== null}

View File

@@ -490,16 +490,15 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
try {
let html: string
if (state.isMarkdown) {
const { marked } = await import('marked')
html = await marked(state.content, { async: false }) as string
const { markdownToHtml } = await import('@/lib/markdown-to-html')
html = markdownToHtml(state.content)
} else {
html = state.content
.split(/\n{2,}/)
.map(para => `<p>${para.trim().replace(/\n/g, '<br />')}</p>`)
.join('')
}
actions.setContent(html)
actions.setIsMarkdown(false)
actions.convertToRichText(html)
toast.success(t('notes.convertedToRichText') || 'Converted to rich text', {
duration: 8000,

View File

@@ -84,6 +84,9 @@ export interface NoteEditorActions {
setShowMarkdownPreview: (show: boolean) => void
setIsMarkdown: (markdown: boolean) => void
/** Bascule atomiquement la note en texte enrichi : applique le HTML immédiatement
* et sort du mode markdown (source unique de conversion markdown → rich text). */
convertToRichText: (html: string) => void
setColor: (color: NoteColor) => void
setSize: (size: NoteSize) => void

View File

@@ -76,6 +76,10 @@ load_env_docker() {
esac
key="${line%%=*}"
val="${line#*=}"
key="${key%%[[:space:]]}" # trim trailing whitespace du key
# skip placeholders et valeurs vides
[ -z "$val" ] && continue
[ "$val" = "???" ] && continue
# strip les guillemets entourants seulement s'ils sont équilibrés
case "$val" in
\"*\") val="${val#\"}"; val="${val%\"}" ;;
@@ -127,9 +131,11 @@ HEALTH_CHECK_SLEEP_SECONDS=5
cd "$ROOT"
# Sanitize .env.docker: Docker Compose v2 rejects ANY quote character
if [ -f "$ROOT/.env.docker" ]; then
tr -d '"' < "$ROOT/.env.docker" > "$ROOT/.env.docker.tmp" && mv "$ROOT/.env.docker.tmp" "$ROOT/.env.docker"
sed -i 's/\r$//' "$ROOT/.env.docker"
# Docker Compose reads .env (not .env.docker) for ${...} interpolation.
# Sync .env from .env.docker so POSTGRES_PASSWORD etc. are interpolated correctly.
grep -v '???' "$ROOT/.env.docker" | grep -v '^#' > "$ROOT/.env"
fi
load_env_docker
@@ -214,7 +220,10 @@ for i in $(seq 1 "$HEALTH_CHECK_MAX_ITERATIONS"); do
git checkout monitoring/metrics-token || echo "default-token-value" > monitoring/metrics-token
fi
load_env_docker
load_env_docker
echo "DEBUG load_env: POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-NOT_SET}"
echo "DEBUG .env.docker: $(grep '^POSTGRES_PASSWORD=' "$ROOT/.env.docker" 2>/dev/null || echo MISSING)"
echo "DEBUG .env file: $(grep '^POSTGRES_PASSWORD=' "$ROOT/.env" 2>/dev/null || echo MISSING)"
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${TELEGRAM_CHAT_ID:-}" ]; then
echo "=== Starting Monitoring Stack (with Telegram bot) ==="
docker compose -f monitoring/docker-compose.monitoring.yml --profile telegram up -d --remove-orphans 2>&1 || echo "WARN: Failed to bring up monitoring stack"