From fdb148144e387e68b6903250b6bc768331e1ed31 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Tue, 19 May 2026 20:07:56 +0000 Subject: [PATCH] fix: restore brainstorming feature with missing socket server and real-time events --- .../49e72a9e-f54f-4798-ba05-2ff82f1069f0.json | 1 + .env.docker.example | 9 + .gitea/workflows/ci.yaml | 12 +- .gitea/workflows/deploy.yaml | 10 + docker-compose.yml | 26 ++ docs/plan-reparation-prod-2026-05-19.md | 329 ++++++++++++++++++ memento-note/Dockerfile.socket | 23 ++ memento-note/Dockerfile.socket.prebuilt | 35 ++ .../brainstorm/[sessionId]/convert/route.ts | 11 + .../brainstorm/[sessionId]/dismiss/route.ts | 9 + .../brainstorm/[sessionId]/expand/route.ts | 13 + .../brainstorm/[sessionId]/finalize/route.ts | 11 + .../[sessionId]/manual-idea/route.ts | 7 + memento-note/hooks/use-brainstorm-socket.ts | 2 +- scripts/deploy-prod.sh | 4 + 15 files changed, 500 insertions(+), 2 deletions(-) create mode 120000 .antigravitycli/49e72a9e-f54f-4798-ba05-2ff82f1069f0.json create mode 100644 docs/plan-reparation-prod-2026-05-19.md create mode 100644 memento-note/Dockerfile.socket create mode 100644 memento-note/Dockerfile.socket.prebuilt diff --git a/.antigravitycli/49e72a9e-f54f-4798-ba05-2ff82f1069f0.json b/.antigravitycli/49e72a9e-f54f-4798-ba05-2ff82f1069f0.json new file mode 120000 index 0000000..44ff823 --- /dev/null +++ b/.antigravitycli/49e72a9e-f54f-4798-ba05-2ff82f1069f0.json @@ -0,0 +1 @@ +/home/devparsa/.gemini/config/projects/49e72a9e-f54f-4798-ba05-2ff82f1069f0.json \ No newline at end of file diff --git a/.env.docker.example b/.env.docker.example index bbc28e5..9732e28 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -110,3 +110,12 @@ OLLAMA_BASE_URL="http://ollama:11434" # RESEND EMAIL (alternative to SMTP, optional) # ============================================================================= # RESEND_API_KEY="re_..." + +# ───────────────────────────────────────────────────────────────────────────── +# Brainstorm / Socket.io +# ───────────────────────────────────────────────────────────────────────────── +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://note.parsanet.org diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index e227e2f..b64203f 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -76,7 +76,7 @@ jobs: tar czf ../web-artifact.tgz \ .next/standalone .next/static public prisma \ node_modules/.prisma node_modules/@prisma node_modules/prisma \ - docker-entrypoint.sh + docker-entrypoint.sh socket-server.ts tsconfig.json - name: Upload web artifact if: github.ref == 'refs/heads/main' && github.event_name == 'push' @@ -141,6 +141,11 @@ jobs: JINA_API_KEY: ${{ secrets.JINA_API_KEY }} AUTH_GOOGLE_ID: ${{ vars.AUTH_GOOGLE_ID }} AUTH_GOOGLE_SECRET: ${{ secrets.AUTH_GOOGLE_SECRET }} + SOCKET_INTERNAL_KEY: ${{ secrets.SOCKET_INTERNAL_KEY }} + SOCKET_PORT: ${{ vars.SOCKET_PORT }} + SOCKET_HTTP_PORT: ${{ vars.SOCKET_HTTP_PORT }} + SOCKET_INTERNAL_URL: ${{ vars.SOCKET_INTERNAL_URL }} + NEXT_PUBLIC_SOCKET_URL: ${{ vars.NEXT_PUBLIC_SOCKET_URL }} run: | ENV_FILE="/opt/memento/.env.docker" touch "$ENV_FILE" @@ -185,6 +190,11 @@ jobs: upsert JINA_API_KEY "$JINA_API_KEY" upsert AUTH_GOOGLE_ID "$AUTH_GOOGLE_ID" upsert AUTH_GOOGLE_SECRET "$AUTH_GOOGLE_SECRET" + upsert SOCKET_INTERNAL_KEY "$SOCKET_INTERNAL_KEY" + upsert SOCKET_PORT "$SOCKET_PORT" + upsert SOCKET_HTTP_PORT "$SOCKET_HTTP_PORT" + upsert SOCKET_INTERNAL_URL "$SOCKET_INTERNAL_URL" + upsert NEXT_PUBLIC_SOCKET_URL "$NEXT_PUBLIC_SOCKET_URL" - name: Deploy on 192.168.1.190 env: diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index f54ac67..ecbd780 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -54,6 +54,11 @@ jobs: JINA_API_KEY: ${{ secrets.JINA_API_KEY }} AUTH_GOOGLE_ID: ${{ vars.AUTH_GOOGLE_ID }} AUTH_GOOGLE_SECRET: ${{ secrets.AUTH_GOOGLE_SECRET }} + SOCKET_INTERNAL_KEY: ${{ secrets.SOCKET_INTERNAL_KEY }} + SOCKET_PORT: ${{ vars.SOCKET_PORT }} + SOCKET_HTTP_PORT: ${{ vars.SOCKET_HTTP_PORT }} + SOCKET_INTERNAL_URL: ${{ vars.SOCKET_INTERNAL_URL }} + NEXT_PUBLIC_SOCKET_URL: ${{ vars.NEXT_PUBLIC_SOCKET_URL }} run: | ENV_FILE="/opt/memento/.env.docker" touch "$ENV_FILE" @@ -98,6 +103,11 @@ jobs: upsert JINA_API_KEY "$JINA_API_KEY" upsert AUTH_GOOGLE_ID "$AUTH_GOOGLE_ID" upsert AUTH_GOOGLE_SECRET "$AUTH_GOOGLE_SECRET" + upsert SOCKET_INTERNAL_KEY "$SOCKET_INTERNAL_KEY" + upsert SOCKET_PORT "$SOCKET_PORT" + upsert SOCKET_HTTP_PORT "$SOCKET_HTTP_PORT" + upsert SOCKET_INTERNAL_URL "$SOCKET_INTERNAL_URL" + upsert NEXT_PUBLIC_SOCKET_URL "$NEXT_PUBLIC_SOCKET_URL" - name: Deploy (full build, no CI artifact) env: diff --git a/docker-compose.yml b/docker-compose.yml index f1cd3ae..d1a0c85 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -88,6 +88,32 @@ services: cpus: '0.5' memory: 512M + # ============================================ + # memento-socket - Socket.io Real-time Server + # ============================================ + memento-socket: + build: + context: ./memento-note + dockerfile: ${MEMENTO_SOCKET_DOCKERFILE:-Dockerfile.socket} + container_name: memento-socket + env_file: + - .env.docker + ports: + - "3002:3002" + environment: + - NODE_ENV=production + restart: unless-stopped + networks: + - memento-network + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + # ============================================ # mcp-server - MCP Protocol Server # ============================================ diff --git a/docs/plan-reparation-prod-2026-05-19.md b/docs/plan-reparation-prod-2026-05-19.md new file mode 100644 index 0000000..52a9cd9 --- /dev/null +++ b/docs/plan-reparation-prod-2026-05-19.md @@ -0,0 +1,329 @@ +# Plan de réparation production — 19 mai 2026 + +## État actuel des problèmes + +1. **Tables brainstorm manquantes en prod** — les 7 tables n'existent pas en base +2. **Migration non enregistrée** — le container a été build avec 17 migrations, la 18e (brainstorm) est dans le code mais pas dans l'image +3. **Redis fonctionne** — le container redis tourne, mais il faut vérifier que memento-web a bien `REDIS_URL` + +## Étape 1 : Créer les tables brainstorm en prod + +Se connecter au serveur en root, puis : + +```bash +docker exec -i memento-postgres psql -U memento -d memento <<'EOSQL' + +-- ======================================== +-- Tables brainstorm +-- ======================================== + +CREATE TABLE IF NOT EXISTS "BrainstormSession" ( + "id" TEXT NOT NULL, + "seedIdea" TEXT NOT NULL, + "sourceNoteId" TEXT, + "contextNoteIds" TEXT, + "exportedNoteId" TEXT, + "userId" TEXT NOT NULL, + "inviteToken" TEXT, + "inviteExpiry" TIMESTAMP(3), + "liveblocksRoomId" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "guestCanEdit" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "BrainstormSession_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "BrainstormIdea" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "waveNumber" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "connectionToSeed" TEXT, + "noveltyScore" INTEGER, + "parentIdeaId" TEXT, + "convertedToNoteId" TEXT, + "relatedNoteIds" TEXT, + "status" TEXT NOT NULL DEFAULT 'active', + "positionX" DOUBLE PRECISION, + "positionY" DOUBLE PRECISION, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT, + "createdByType" TEXT DEFAULT 'ai', + CONSTRAINT "BrainstormIdea_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "BrainstormNoteRef" ( + "id" TEXT NOT NULL, + "ideaId" TEXT NOT NULL, + "noteId" TEXT, + "relation" TEXT NOT NULL, + "explanation" TEXT NOT NULL, + "verdict" TEXT NOT NULL DEFAULT 'unresolved', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "visibility" TEXT NOT NULL DEFAULT 'participants', + CONSTRAINT "BrainstormNoteRef_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "BrainstormParticipant" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'viewer', + "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BrainstormParticipant_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "BrainstormActivity" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "userId" TEXT, + "action" TEXT NOT NULL, + "details" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BrainstormActivity_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "BrainstormShare" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "sharedBy" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "permission" TEXT NOT NULL DEFAULT 'editor', + "notifiedAt" TIMESTAMP(3), + "respondedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "BrainstormShare_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "BrainstormSnapshot" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "activityId" TEXT, + "step" INTEGER NOT NULL, + "label" TEXT, + "ideaGraph" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BrainstormSnapshot_pkey" PRIMARY KEY ("id") +); + +-- ======================================== +-- Index uniques +-- ======================================== + +CREATE UNIQUE INDEX IF NOT EXISTS "BrainstormSession_inviteToken_key" ON "BrainstormSession"("inviteToken"); +CREATE UNIQUE INDEX IF NOT EXISTS "BrainstormParticipant_sessionId_userId_key" ON "BrainstormParticipant"("sessionId", "userId"); +CREATE UNIQUE INDEX IF NOT EXISTS "BrainstormShare_sessionId_userId_key" ON "BrainstormShare"("sessionId", "userId"); + +-- ======================================== +-- Index de performance +-- ======================================== + +CREATE INDEX IF NOT EXISTS "BrainstormSession_userId_idx" ON "BrainstormSession"("userId"); +CREATE INDEX IF NOT EXISTS "BrainstormSession_userId_createdAt_idx" ON "BrainstormSession"("userId", "createdAt"); +CREATE INDEX IF NOT EXISTS "BrainstormSession_inviteToken_idx" ON "BrainstormSession"("inviteToken"); +CREATE INDEX IF NOT EXISTS "BrainstormSession_isPublic_idx" ON "BrainstormSession"("isPublic"); +CREATE INDEX IF NOT EXISTS "BrainstormIdea_sessionId_idx" ON "BrainstormIdea"("sessionId"); +CREATE INDEX IF NOT EXISTS "BrainstormIdea_waveNumber_idx" ON "BrainstormIdea"("waveNumber"); +CREATE INDEX IF NOT EXISTS "BrainstormIdea_status_idx" ON "BrainstormIdea"("status"); +CREATE INDEX IF NOT EXISTS "BrainstormIdea_parentIdeaId_idx" ON "BrainstormIdea"("parentIdeaId"); +CREATE INDEX IF NOT EXISTS "BrainstormIdea_sessionId_status_idx" ON "BrainstormIdea"("sessionId", "status"); +CREATE INDEX IF NOT EXISTS "BrainstormIdea_sessionId_waveNumber_createdAt_idx" ON "BrainstormIdea"("sessionId", "waveNumber", "createdAt"); +CREATE INDEX IF NOT EXISTS "BrainstormNoteRef_ideaId_idx" ON "BrainstormNoteRef"("ideaId"); +CREATE INDEX IF NOT EXISTS "BrainstormNoteRef_noteId_idx" ON "BrainstormNoteRef"("noteId"); +CREATE INDEX IF NOT EXISTS "BrainstormParticipant_sessionId_idx" ON "BrainstormParticipant"("sessionId"); +CREATE INDEX IF NOT EXISTS "BrainstormParticipant_userId_idx" ON "BrainstormParticipant"("userId"); +CREATE INDEX IF NOT EXISTS "BrainstormActivity_sessionId_createdAt_idx" ON "BrainstormActivity"("sessionId", "createdAt"); +CREATE INDEX IF NOT EXISTS "BrainstormShare_userId_idx" ON "BrainstormShare"("userId"); +CREATE INDEX IF NOT EXISTS "BrainstormShare_status_idx" ON "BrainstormShare"("status"); +CREATE INDEX IF NOT EXISTS "BrainstormSnapshot_sessionId_step_idx" ON "BrainstormSnapshot"("sessionId", "step"); +CREATE INDEX IF NOT EXISTS "BrainstormSnapshot_sessionId_createdAt_idx" ON "BrainstormSnapshot"("sessionId", "createdAt"); + +-- ======================================== +-- Clés étrangères +-- ======================================== + +DO $$ BEGIN + ALTER TABLE "BrainstormSession" ADD CONSTRAINT "BrainstormSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormSession" ADD CONSTRAINT "BrainstormSession_sourceNoteId_fkey" FOREIGN KEY ("sourceNoteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormSession" ADD CONSTRAINT "BrainstormSession_exportedNoteId_fkey" FOREIGN KEY ("exportedNoteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormIdea" ADD CONSTRAINT "BrainstormIdea_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormIdea" ADD CONSTRAINT "BrainstormIdea_parentIdeaId_fkey" FOREIGN KEY ("parentIdeaId") REFERENCES "BrainstormIdea"("id") ON DELETE SET NULL ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormIdea" ADD CONSTRAINT "BrainstormIdea_convertedToNoteId_fkey" FOREIGN KEY ("convertedToNoteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormNoteRef" ADD CONSTRAINT "BrainstormNoteRef_ideaId_fkey" FOREIGN KEY ("ideaId") REFERENCES "BrainstormIdea"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormNoteRef" ADD CONSTRAINT "BrainstormNoteRef_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormParticipant" ADD CONSTRAINT "BrainstormParticipant_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormParticipant" ADD CONSTRAINT "BrainstormParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormActivity" ADD CONSTRAINT "BrainstormActivity_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormActivity" ADD CONSTRAINT "BrainstormActivity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormShare" ADD CONSTRAINT "BrainstormShare_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormShare" ADD CONSTRAINT "BrainstormShare_sharedBy_fkey" FOREIGN KEY ("sharedBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormShare" ADD CONSTRAINT "BrainstormShare_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "BrainstormSnapshot" ADD CONSTRAINT "BrainstormSnapshot_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- ======================================== +-- Enregistrer la migration dans Prisma +-- ======================================== + +INSERT INTO "_prisma_migrations" (id, checksum, migration_name, logs, started_at, finished_at, applied_steps_count) +VALUES ( + gen_random_uuid(), + 'manual-creation-20260519', + '20260519120000_add_brainstorm_tables', + NULL, + now(), + now(), + 1 +) ON CONFLICT DO NOTHING; + +EOSQL +``` + +## Étape 2 : Vérifier que les tables sont créées + +```bash +docker exec memento-postgres psql -U memento -d memento -c "SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename LIKE '%rainstorm%' ORDER BY tablename;" +``` + +Résultat attendu : 7 lignes (BrainstormActivity, BrainstormIdea, BrainstormNoteRef, BrainstormParticipant, BrainstormSession, BrainstormShare, BrainstormSnapshot) + +## Étape 3 : Vérifier les migrations enregistrées + +```bash +docker exec memento-postgres psql -U memento -d memento -c "SELECT migration_name FROM _prisma_migrations ORDER BY started_at DESC LIMIT 3;" +``` + +Résultat attendu : la migration `20260519120000_add_brainstorm_tables` doit apparaître. + +## Étape 4 : Vérifier Redis + +```bash +docker ps | grep redis +docker exec memento-redis redis-cli ping +``` + +Résultat attendu : `PONG` + +Vérifier que memento-web a bien la variable REDIS_URL : + +```bash +docker exec memento-web env | grep REDIS +``` + +Résultat attendu : `REDIS_URL=redis://redis:6379` + +Si vide, il faut recréer le container : + +```bash +docker compose up -d --force-recreate memento-note +``` + +## Étape 5 : Redémarrer l'app + +```bash +docker compose restart memento-note +``` + +## Étape 6 : Vérifier les logs + +```bash +sleep 10 +docker logs memento-web --tail 30 2>&1 | grep -i -E "error|redis|brainstorm" +``` + +Résultat attendu : +- Aucune erreur `BrainstormSession does not exist` +- Aucune erreur `redis ECONNREFUSED` +- `[redis] Connected` doit apparaître + +## Étape 7 : Tester le brainstorm + +1. Ouvrir https://note.parsanet.org/brainstorm +2. Taper une idée et cliquer le bouton + +3. Vérifier que les 9 idées apparaissent sur le canvas + +--- + +## Si l'étape 1 échoue + +Copier le message d'erreur exact. Les causes possibles : +- Permission denied → il faut être root (`sudo -i` avant) +- duplicate_object → tables déjà créées partiellement, aller à l'étape 2 pour vérifier +- Column exists → certaines colonnes existent déjà, les `IF NOT EXISTS` gèrent ça + +## Si le brainstorm charge indéfiniment + +Vérifier les logs en temps réel : + +```bash +docker logs memento-web -f --tail 50 2>&1 & +``` + +Puis lancer un brainstorm et regarder les logs. Copier le message d'erreur exact. diff --git a/memento-note/Dockerfile.socket b/memento-note/Dockerfile.socket new file mode 100644 index 0000000..2766e1d --- /dev/null +++ b/memento-note/Dockerfile.socket @@ -0,0 +1,23 @@ +FROM node:22-bookworm-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +COPY package.json package-lock.json* ./ +RUN npm install + +# We only need the socket server and dependencies +COPY socket-server.ts ./ +COPY tsconfig.json ./ + +# Environment defaults +ENV NODE_ENV=production +ENV SOCKET_PORT=3002 +ENV SOCKET_HTTP_PORT=3003 + +# Run with tsx (included in devDependencies but we need it at runtime here) +# Alternatively, we could build it to JS, but tsx is easier for this standalone file. +CMD ["npx", "tsx", "socket-server.ts"] diff --git a/memento-note/Dockerfile.socket.prebuilt b/memento-note/Dockerfile.socket.prebuilt new file mode 100644 index 0000000..c540aac --- /dev/null +++ b/memento-note/Dockerfile.socket.prebuilt @@ -0,0 +1,35 @@ +FROM node:22-bookworm-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +# In prebuilt mode, we expect node_modules to be partially there or we use the standalone's ones +# Actually, the artifact has node_modules/.prisma etc. but not the full node_modules. +# However, the standalone folder HAS node_modules. +# But we need tsx and socket.io. +# So we still need a minimal npm install or just use the pre-installed ones if we can. + +# Let's keep it simple: if it's prebuilt, we just copy what we need. +# But tsx is a devDependency. +# Better to just use the full Dockerfile.socket for now as it's more reliable. +# "tsx" is not in the standalone output. + +# Wait, I can just use the same strategy as memento-note: +# if ARTIFACT_TGZ is present, we already tar'ed the files. +# But we still need to run them. + +COPY socket-server.ts ./ +COPY tsconfig.json ./ +COPY package.json package-lock.json* ./ + +# We still need to install dependencies because standalone doesn't have all of them (like tsx) +RUN npm install --only=production && npm install tsx + +ENV NODE_ENV=production +ENV SOCKET_PORT=3002 +ENV SOCKET_HTTP_PORT=3003 + +CMD ["npx", "tsx", "socket-server.ts"] diff --git a/memento-note/app/api/brainstorm/[sessionId]/convert/route.ts b/memento-note/app/api/brainstorm/[sessionId]/convert/route.ts index a1feca9..9617747 100644 --- a/memento-note/app/api/brainstorm/[sessionId]/convert/route.ts +++ b/memento-note/app/api/brainstorm/[sessionId]/convert/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { auth } from '@/auth' import { z } from 'zod' +import { logActivity } from '@/lib/brainstorm-collab' +import { emitToSession } from '@/lib/socket-emit' const convertSchema = z.object({ ideaId: z.string().min(1), @@ -153,6 +155,15 @@ export async function POST( await prisma.$transaction(tagPromises) + await logActivity(sessionId, 'idea_converted', session.user.id, { ideaTitle: idea.title, ideaId: idea.id, noteId: note.id }) + + await emitToSession(sessionId, 'activity:new', { + action: 'idea_converted', + userId: session.user.id, + userName: session.user.name || 'Guest', + details: { ideaTitle: idea.title, ideaId: idea.id, noteId: note.id } + }) + return NextResponse.json({ success: true, data: note }, { status: 201 }) } catch (error: any) { if (error instanceof z.ZodError) { diff --git a/memento-note/app/api/brainstorm/[sessionId]/dismiss/route.ts b/memento-note/app/api/brainstorm/[sessionId]/dismiss/route.ts index 98b74e5..0f98a40 100644 --- a/memento-note/app/api/brainstorm/[sessionId]/dismiss/route.ts +++ b/memento-note/app/api/brainstorm/[sessionId]/dismiss/route.ts @@ -3,6 +3,7 @@ import prisma from '@/lib/prisma' import { auth } from '@/auth' import { z } from 'zod' import { verifyParticipant, logActivity, captureSnapshot } from '@/lib/brainstorm-collab' +import { emitToSession } from '@/lib/socket-emit' const dismissSchema = z.object({ ideaId: z.string().min(1), @@ -56,6 +57,14 @@ export async function POST( await logActivity(sessionId, 'idea_dismissed', session.user.id, { ideaTitle: idea.title }) + await emitToSession(sessionId, 'idea:dismissed', { ideaId, userId: session.user.id }) + await emitToSession(sessionId, 'activity:new', { + action: 'idea_dismissed', + userId: session.user.id, + userName: session.user.name || 'Guest', + details: { ideaTitle: idea.title, ideaId } + }) + await captureSnapshot(sessionId, `Dismissed: ${idea.title}`).catch(() => {}) return NextResponse.json({ success: true }) diff --git a/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts b/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts index 37e029d..19e778d 100644 --- a/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts +++ b/memento-note/app/api/brainstorm/[sessionId]/expand/route.ts @@ -15,7 +15,9 @@ import { resolveAiContextUserId, sanitizeNotesForGuest, captureSnapshot, + logActivity, } from '@/lib/brainstorm-collab' +import { emitToSession } from '@/lib/socket-emit' const expandSchema = z.object({ ideaId: z.string().min(1), @@ -332,6 +334,17 @@ export async function POST( for (const idea of updatedSession?.ideas || []) { (idea as any).creator = (idea as any).createdBy ? cm.get((idea as any).createdBy) || null : null } } + await logActivity(sessionId, 'wave_generated', session.user.id, { count: newIdeas.length, parentIdeaId: ideaId }) + + await emitToSession(sessionId, 'activity:new', { + action: 'wave_generated', + userId: session.user.id, + userName: session.user.name || 'Guest', + details: { count: newIdeas.length, parentIdeaId: ideaId } + }) + // Trigger refetch for all participants + await emitToSession(sessionId, 'idea:added', {}) + await captureSnapshot(sessionId, `Wave expanded: ${parentIdea.title}`).catch(() => {}) return NextResponse.json({ success: true, data: updatedSession }) diff --git a/memento-note/app/api/brainstorm/[sessionId]/finalize/route.ts b/memento-note/app/api/brainstorm/[sessionId]/finalize/route.ts index 4aff643..d374764 100644 --- a/memento-note/app/api/brainstorm/[sessionId]/finalize/route.ts +++ b/memento-note/app/api/brainstorm/[sessionId]/finalize/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { auth } from '@/auth' +import { logActivity } from '@/lib/brainstorm-collab' +import { emitToSession } from '@/lib/socket-emit' export async function POST( request: NextRequest, @@ -80,6 +82,15 @@ export async function POST( const dry = Array.from(noteImpactMap.values()).filter(n => n.acceptedCount === 0 && n.dismissedCount > 0).length const totalRefs = Array.from(noteImpactMap.values()).length + await logActivity(sessionId, 'session_finalized', session.user.id, { notesEnriched: fruitful, notesMarkedDry: dry }) + + await emitToSession(sessionId, 'activity:new', { + action: 'session_finalized', + userId: session.user.id, + userName: session.user.name || 'Guest', + details: { notesEnriched: fruitful, notesMarkedDry: dry } + }) + return NextResponse.json({ success: true, impact: { diff --git a/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts b/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts index 97f81bf..1577182 100644 --- a/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts +++ b/memento-note/app/api/brainstorm/[sessionId]/manual-idea/route.ts @@ -177,6 +177,13 @@ export async function POST( await logActivity(sessionId, 'manual_idea', session.user.id, { ideaTitle: title, ideaId: idea.id }) + await emitToSession(sessionId, 'activity:new', { + action: 'manual_idea', + userId: session.user.id, + userName: session.user.name || 'Guest', + details: { ideaTitle: title, ideaId: idea.id } + }) + await captureSnapshot(sessionId, `Manual idea: ${title}`).catch(() => {}) // [UPDATE - TEMPS RÉEL] Retourner immédiatement, enrichissement IA en arrière-plan diff --git a/memento-note/hooks/use-brainstorm-socket.ts b/memento-note/hooks/use-brainstorm-socket.ts index 5b4276d..7bca7e5 100644 --- a/memento-note/hooks/use-brainstorm-socket.ts +++ b/memento-note/hooks/use-brainstorm-socket.ts @@ -17,7 +17,7 @@ export interface ActivityEvent { details: any } -const SOCKET_URL = process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3001' +const SOCKET_URL = process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3002' export function useBrainstormSocket( sessionId: string | null, diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh index a77b8d7..af6e362 100755 --- a/scripts/deploy-prod.sh +++ b/scripts/deploy-prod.sh @@ -48,18 +48,22 @@ if [ -n "$ARTIFACT_TGZ" ] && [ -f "$ARTIFACT_TGZ" ]; then echo "=== Fast image (CI artifact) ===" tar xzf "$ARTIFACT_TGZ" -C memento-note/ export MEMENTO_DOCKERFILE=Dockerfile.prebuilt + export MEMENTO_SOCKET_DOCKERFILE=Dockerfile.socket.prebuilt else echo "=== Full docker build (no artifact) ===" export MEMENTO_DOCKERFILE=Dockerfile + export MEMENTO_SOCKET_DOCKERFILE=Dockerfile.socket fi docker compose build memento-note +docker compose build memento-socket if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^mcp-server/'; then docker compose build mcp-server fi docker compose up -d --remove-orphans --force-recreate memento-note +docker compose up -d memento-socket docker compose up -d mcp-server 2>/dev/null || true echo "=== Migrations (Prisma CLI via node, pas npx) ==="