diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..d780c99 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(docker compose *)", + "mcp__web-search-prime__web_search_prime", + "WebSearch", + "mcp__zread__search_doc", + "mcp__zread__read_file", + "Bash(npm ls *)", + "mcp__zread__get_repo_structure", + "Bash(npm install *)", + "Bash(docker exec *)" + ] + } +} diff --git a/.kilo/agent-manager.json b/.kilo/agent-manager.json new file mode 100644 index 0000000..93d494d --- /dev/null +++ b/.kilo/agent-manager.json @@ -0,0 +1,9 @@ +{ + "worktrees": {}, + "sessions": {}, + "tabOrder": { + "local": [ + "pending:1" + ] + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 93b3fd1..403c59e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: volumes: - postgres-data:/var/lib/postgresql/data ports: - - "127.0.0.1:5432:5432" + - "127.0.0.1:${POSTGRES_PORT:-5433}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-memento}"] interval: 5s diff --git a/memento-note/.dockerignore b/memento-note/.dockerignore index f6571d5..7acfa64 100644 --- a/memento-note/.dockerignore +++ b/memento-note/.dockerignore @@ -51,3 +51,8 @@ Dockerfile .dockerignore docker-compose.yml deploy.sh + +# Stale service worker artifacts +public/sw.js +public/sw.js.map +public/workbox-*.js diff --git a/memento-note/.gitignore b/memento-note/.gitignore index 9b1afb1..4044d49 100644 --- a/memento-note/.gitignore +++ b/memento-note/.gitignore @@ -46,3 +46,9 @@ next-env.d.ts # generated /prisma/client-generated /_backup + +# Service worker (generated at build time if PWA is re-enabled) +public/sw.js +public/sw.js.map +public/workbox-*.js +public/workbox-*.js.map diff --git a/memento-note/Dockerfile b/memento-note/Dockerfile index 02ffece..b8c9fd1 100644 --- a/memento-note/Dockerfile +++ b/memento-note/Dockerfile @@ -1,63 +1,72 @@ -# Multi-stage build for Next.js 16 with Webpack + Prisma -# Using Debian 11 (bullseye) for native OpenSSL 1.1.x support - -FROM node:22-bullseye-slim AS base - -FROM base AS deps +# =========================================================================== +# Stage 1: Dependencies +# =========================================================================== +FROM node:22-bookworm-slim AS deps WORKDIR /app -# Install OpenSSL (1.1.x native in Debian 11) RUN apt-get update && apt-get install -y --no-install-recommends \ openssl \ && rm -rf /var/lib/apt/lists/* -# Install dependencies COPY package.json package-lock.json* ./ -RUN npm install --legacy-peer-deps +COPY prisma ./prisma -FROM base AS builder +RUN npm install +RUN npx prisma generate + +# =========================================================================== +# Stage 2: Build +# =========================================================================== +FROM node:22-bookworm-slim AS builder WORKDIR /app + COPY --from=deps /app/node_modules ./node_modules COPY . . -# Copy Prisma schema and generate client BEFORE Next.js build -COPY prisma ./prisma -RUN npx prisma generate - -# Build Next.js with Webpack +# PrismaClient validates DATABASE_URL format at import time. +# No actual DB connection occurs during build (all pages are dynamic). +ENV DATABASE_URL="postgresql://build:build@localhost:5432/build" ENV NEXT_TELEMETRY_DISABLED=1 + RUN npm run build -FROM base AS runner +# =========================================================================== +# Stage 3: Runner +# =========================================================================== +FROM node:22-bookworm-slim AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssl \ + && rm -rf /var/lib/apt/lists/* +RUN groupadd --system --gid 1001 nodejs +RUN useradd --system --uid 1001 --gid nodejs nextjs + +# Static assets COPY --from=builder /app/public ./public -COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/package-lock.json ./package-lock.json -RUN mkdir .next -RUN chown nextjs:nodejs .next - -# Copy Next.js standalone output +# Next.js standalone output COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -# Copy Prisma schema, generated client, and Query Engine binaries -COPY --from=builder /app/prisma ./prisma -RUN chown -R nextjs:nodejs /app/prisma +# Prisma: schema + migrations + generated client + CLI +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/prisma ./node_modules/prisma + +# Entrypoint +COPY --from=builder --chown=nextjs:nodejs /app/docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x ./docker-entrypoint.sh USER nextjs EXPOSE 3000 - ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/memento-note/app/(main)/admin/ai-test/ai-tester.tsx b/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx similarity index 100% rename from memento-note/app/(main)/admin/ai-test/ai-tester.tsx rename to memento-note/app/(admin)/admin/ai-test/ai-tester.tsx diff --git a/memento-note/app/(main)/admin/ai-test/page.tsx b/memento-note/app/(admin)/admin/ai-test/page.tsx similarity index 97% rename from memento-note/app/(main)/admin/ai-test/page.tsx rename to memento-note/app/(admin)/admin/ai-test/page.tsx index 3198c82..d520d92 100644 --- a/memento-note/app/(main)/admin/ai-test/page.tsx +++ b/memento-note/app/(admin)/admin/ai-test/page.tsx @@ -2,7 +2,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import Link from 'next/link' import { ArrowLeft, TestTube } from 'lucide-react' import { AI_TESTER } from './ai-tester' import { useLanguage } from '@/lib/i18n' @@ -14,11 +13,11 @@ export default function AITestPage() {
- + - +

diff --git a/memento-note/app/(main)/admin/ai/page.tsx b/memento-note/app/(admin)/admin/ai/page.tsx similarity index 98% rename from memento-note/app/(main)/admin/ai/page.tsx rename to memento-note/app/(admin)/admin/ai/page.tsx index 6d4b6a8..78b3b9d 100644 --- a/memento-note/app/(main)/admin/ai/page.tsx +++ b/memento-note/app/(admin)/admin/ai/page.tsx @@ -1,7 +1,6 @@ import { AdminMetrics } from '@/components/admin-metrics' import { Button } from '@/components/ui/button' import { Zap, Settings, Activity, TrendingUp } from 'lucide-react' -import Link from 'next/link' import { getSystemConfig } from '@/lib/config' export default async function AdminAIPage() { @@ -63,12 +62,12 @@ export default async function AdminAIPage() { Monitor and configure AI features

- + - +
diff --git a/memento-note/app/(main)/admin/create-user-dialog.tsx b/memento-note/app/(admin)/admin/create-user-dialog.tsx similarity index 100% rename from memento-note/app/(main)/admin/create-user-dialog.tsx rename to memento-note/app/(admin)/admin/create-user-dialog.tsx diff --git a/memento-note/app/(admin)/admin/error.tsx b/memento-note/app/(admin)/admin/error.tsx new file mode 100644 index 0000000..50de3b6 --- /dev/null +++ b/memento-note/app/(admin)/admin/error.tsx @@ -0,0 +1,30 @@ +'use client' + +import { useEffect } from 'react' +import { Button } from '@/components/ui/button' + +export default function AdminError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + console.error('Admin route error:', error) + }, [error]) + + return ( +
+

Une erreur est survenue dans l'administration

+

+ Le rendu de cette page a echoue. Vous pouvez reessayer sans recharger toute l'application. +

+
+ +
+
+ ) +} diff --git a/memento-note/app/(admin)/admin/layout.tsx b/memento-note/app/(admin)/admin/layout.tsx new file mode 100644 index 0000000..82807fc --- /dev/null +++ b/memento-note/app/(admin)/admin/layout.tsx @@ -0,0 +1,22 @@ +import { AdminHeader } from '@/components/admin-header' +import { AdminSidebar } from '@/components/admin-sidebar' +import { AdminContentArea } from '@/components/admin-content-area' + +// Auth is enforced solely by middleware (auth.config.ts → authorized callback). +// All cross-group navigation (admin ↔ main) uses tags (full page reload) +// to avoid React Error #310 caused by Next.js 16.x route-group transition bug. +export default function AdminLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ +
+ + {children} +
+
+ ) +} diff --git a/memento-note/app/(main)/admin/page.tsx b/memento-note/app/(admin)/admin/page.tsx similarity index 100% rename from memento-note/app/(main)/admin/page.tsx rename to memento-note/app/(admin)/admin/page.tsx diff --git a/memento-note/app/(main)/admin/settings/admin-settings-form.tsx b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx similarity index 96% rename from memento-note/app/(main)/admin/settings/admin-settings-form.tsx rename to memento-note/app/(admin)/admin/settings/admin-settings-form.tsx index eccb94d..f8348c1 100644 --- a/memento-note/app/(main)/admin/settings/admin-settings-form.tsx +++ b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx @@ -7,11 +7,8 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { Label } from '@/components/ui/label' import { Combobox } from '@/components/ui/combobox' import { updateSystemConfig, testEmail } from '@/app/actions/admin-settings' -import { getOllamaModels } from '@/app/actions/ollama' -import { getCustomModels, getCustomEmbeddingModels } from '@/app/actions/custom-provider' import { toast } from 'sonner' import { useState, useEffect, useCallback } from 'react' -import Link from 'next/link' import { TestTube, ExternalLink, RefreshCw } from 'lucide-react' import { useLanguage } from '@/lib/i18n' @@ -73,7 +70,8 @@ export function AdminSettingsForm({ config }: { config: Record } const [isLoadingCustomEmbeddingsModels, setIsLoadingCustomEmbeddingsModels] = useState(false) const [isLoadingCustomChatModels, setIsLoadingCustomChatModels] = useState(false) - // Fetch Ollama models + // Fetch Ollama models via Route API (not Server Action) to avoid App Router + // action queue dispatch during render, which causes React Error #310. const fetchOllamaModels = useCallback(async (type: 'tags' | 'embeddings' | 'chat', url: string) => { if (!url) return @@ -82,7 +80,9 @@ export function AdminSettingsForm({ config }: { config: Record } else setIsLoadingChatModels(true) try { - const result = await getOllamaModels(url) + const params = new URLSearchParams({ type: 'ollama', url }) + const res = await fetch(`/api/admin/models?${params}`) + const result = await res.json() if (result.success) { if (type === 'tags') setOllamaTagsModels(result.models) @@ -101,7 +101,7 @@ export function AdminSettingsForm({ config }: { config: Record } } }, []) - // Fetch Custom provider models — tags use /v1/models, embeddings use /v1/embeddings/models + // Fetch Custom provider models via Route API (not Server Action). const fetchCustomModels = useCallback(async (type: 'tags' | 'embeddings' | 'chat', url: string, apiKey?: string) => { if (!url) return @@ -110,9 +110,10 @@ export function AdminSettingsForm({ config }: { config: Record } else setIsLoadingCustomChatModels(true) try { - const result = type === 'embeddings' - ? await getCustomEmbeddingModels(url, apiKey) - : await getCustomModels(url, apiKey) + const params = new URLSearchParams({ type: 'custom', url, kind: type }) + if (apiKey) params.set('key', apiKey) + const res = await fetch(`/api/admin/models?${params}`) + const result = await res.json() if (result.success && result.models.length > 0) { if (type === 'tags') setCustomTagsModels(result.models) @@ -131,47 +132,38 @@ export function AdminSettingsForm({ config }: { config: Record } } }, []) - // Initial fetch for Ollama models if provider is selected + // Single consolidated effect for initial model fetching. + // Batching all provider checks into ONE effect prevents multiple Server Actions + // from being dispatched simultaneously, which would cause React Error #310 by + // flooding the App Router's action queue during an ongoing navigation transition. useEffect(() => { - if (tagsProvider === 'ollama') { - const url = config.OLLAMA_BASE_URL_TAGS || config.OLLAMA_BASE_URL || 'http://localhost:11434' - fetchOllamaModels('tags', url) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tagsProvider]) + const fetchInitialModels = async () => { + const ollamaBase = config.OLLAMA_BASE_URL || 'http://localhost:11434' + const customUrl = config.CUSTOM_OPENAI_BASE_URL || '' + const customKey = config.CUSTOM_OPENAI_API_KEY || '' - useEffect(() => { - if (embeddingsProvider === 'ollama') { - const url = config.OLLAMA_BASE_URL_EMBEDDING || config.OLLAMA_BASE_URL || 'http://localhost:11434' - fetchOllamaModels('embeddings', url) - } else if (embeddingsProvider === 'custom') { - const url = config.CUSTOM_OPENAI_BASE_URL || '' - const key = config.CUSTOM_OPENAI_API_KEY || '' - if (url) fetchCustomModels('embeddings', url, key) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [embeddingsProvider]) + if (tagsProvider === 'ollama') { + await fetchOllamaModels('tags', config.OLLAMA_BASE_URL_TAGS || ollamaBase) + } else if (tagsProvider === 'custom' && customUrl) { + await fetchCustomModels('tags', customUrl, customKey) + } - useEffect(() => { - if (tagsProvider === 'custom') { - const url = config.CUSTOM_OPENAI_BASE_URL || '' - const key = config.CUSTOM_OPENAI_API_KEY || '' - if (url) fetchCustomModels('tags', url, key) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tagsProvider]) + if (embeddingsProvider === 'ollama') { + await fetchOllamaModels('embeddings', config.OLLAMA_BASE_URL_EMBEDDING || ollamaBase) + } else if (embeddingsProvider === 'custom' && customUrl) { + await fetchCustomModels('embeddings', customUrl, customKey) + } - useEffect(() => { - if (chatProvider === 'ollama') { - const url = config.OLLAMA_BASE_URL_CHAT || config.OLLAMA_BASE_URL || 'http://localhost:11434' - fetchOllamaModels('chat', url) - } else if (chatProvider === 'custom') { - const url = config.CUSTOM_OPENAI_BASE_URL || '' - const key = config.CUSTOM_OPENAI_API_KEY || '' - if (url) fetchCustomModels('chat', url, key) + if (chatProvider === 'ollama') { + await fetchOllamaModels('chat', config.OLLAMA_BASE_URL_CHAT || ollamaBase) + } else if (chatProvider === 'custom' && customUrl) { + await fetchCustomModels('chat', customUrl, customKey) + } } + + fetchInitialModels() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chatProvider]) + }, []) const handleSaveSecurity = async (formData: FormData) => { setIsSaving(true) @@ -876,13 +868,13 @@ export function AdminSettingsForm({ config }: { config: Record } - +
- + diff --git a/memento-note/app/(main)/admin/settings/page.tsx b/memento-note/app/(admin)/admin/settings/page.tsx similarity index 100% rename from memento-note/app/(main)/admin/settings/page.tsx rename to memento-note/app/(admin)/admin/settings/page.tsx diff --git a/memento-note/app/(main)/admin/user-list.tsx b/memento-note/app/(admin)/admin/user-list.tsx similarity index 100% rename from memento-note/app/(main)/admin/user-list.tsx rename to memento-note/app/(admin)/admin/user-list.tsx diff --git a/memento-note/app/(main)/admin/users/page.tsx b/memento-note/app/(admin)/admin/users/page.tsx similarity index 100% rename from memento-note/app/(main)/admin/users/page.tsx rename to memento-note/app/(admin)/admin/users/page.tsx diff --git a/memento-note/app/(admin)/layout.tsx b/memento-note/app/(admin)/layout.tsx new file mode 100644 index 0000000..8742b00 --- /dev/null +++ b/memento-note/app/(admin)/layout.tsx @@ -0,0 +1,24 @@ +import { AdminProvidersWrapper } from '@/components/admin-providers-wrapper' +import { detectUserLanguage } from '@/lib/i18n/detect-user-language' +import { loadTranslations } from '@/lib/i18n/load-translations' + +// No here intentionally: combining a Suspense boundary with +// components inside the subtree triggers a React production-only bug (React +// issue #33580, Next.js issue #63388) causing Error #310 "too many re-renders". +export default async function AdminGroupLayout({ + children, +}: { + children: React.ReactNode +}) { + const initialLanguage = await detectUserLanguage() + const initialTranslations = await loadTranslations(initialLanguage) + + return ( + + {children} + + ) +} diff --git a/memento-note/app/(main)/admin/layout.tsx b/memento-note/app/(main)/admin/layout.tsx deleted file mode 100644 index df2f12b..0000000 --- a/memento-note/app/(main)/admin/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { AdminSidebar } from '@/components/admin-sidebar' -import { AdminContentArea } from '@/components/admin-content-area' -import { auth } from '@/auth' -import { redirect } from 'next/navigation' - -export default async function AdminLayout({ - children, -}: { - children: React.ReactNode -}) { - const session = await auth() - - if ((session?.user as any)?.role !== 'ADMIN') { - redirect('/') - } - - return ( -
- - {children} -
- ) -} diff --git a/memento-note/app/(main)/admin/loading.tsx b/memento-note/app/(main)/admin/loading.tsx deleted file mode 100644 index 4df6844..0000000 --- a/memento-note/app/(main)/admin/loading.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export default function AdminLoading() { - return ( -
-
-
-
-
- {[1, 2, 3].map((i) => ( -
-
-
-
-
-
-
-
- ))} -
- ) -} diff --git a/memento-note/app/(main)/layout.tsx b/memento-note/app/(main)/layout.tsx index e8ba797..11ce662 100644 --- a/memento-note/app/(main)/layout.tsx +++ b/memento-note/app/(main)/layout.tsx @@ -1,3 +1,4 @@ +import { Suspense } from "react"; import { HeaderWrapper } from "@/components/header-wrapper"; import { Sidebar } from "@/components/sidebar"; import { ProvidersWrapper } from "@/components/providers-wrapper"; @@ -28,7 +29,9 @@ export default async function MainLayout({ {/* Main Layout */}
{/* Sidebar Navigation - Style Keep */} - + }> + + {/* Main Content Area */}
diff --git a/memento-note/app/(main)/settings/loading.tsx b/memento-note/app/(main)/settings/loading.tsx deleted file mode 100644 index f3d5fd4..0000000 --- a/memento-note/app/(main)/settings/loading.tsx +++ /dev/null @@ -1,26 +0,0 @@ -export default function SettingsLoading() { - return ( -
-
-
-
-
- {[1, 2, 3].map((i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} -
- ) -} diff --git a/memento-note/app/actions/register.ts b/memento-note/app/actions/register.ts index 777db30..9154ebe 100644 --- a/memento-note/app/actions/register.ts +++ b/memento-note/app/actions/register.ts @@ -41,11 +41,15 @@ export async function register(prevState: string | undefined, formData: FormData const hashedPassword = await bcrypt.hash(password, 10); + const adminEmail = process.env.ADMIN_EMAIL?.toLowerCase(); + const role = adminEmail && email.toLowerCase() === adminEmail ? 'ADMIN' : 'USER'; + await prisma.user.create({ data: { email: email.toLowerCase(), password: hashedPassword, name, + role, }, }); diff --git a/memento-note/app/api/admin/models/route.ts b/memento-note/app/api/admin/models/route.ts new file mode 100644 index 0000000..361c8dc --- /dev/null +++ b/memento-note/app/api/admin/models/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' + +async function requireAdmin() { + const session = await auth() + if (!session?.user?.id || (session.user as any).role !== 'ADMIN') return null + return session +} + +/** + * GET /api/admin/models?type=ollama&url= + * GET /api/admin/models?type=custom&url=&key=&kind=tags|embeddings + * + * Route API (not a Server Action) for fetching AI model lists from Ollama or + * OpenAI-compatible providers. Using a Route Handler instead of a Server Action + * is the correct architecture for client-side GET requests: Server Actions are + * for data mutations, and calling them from useEffect pushes items into the + * App Router's internal action queue, which is drained during render (inside + * AppRouter's useMemo), triggering React Error #310 when multiple calls stack up. + */ +export async function GET(request: NextRequest) { + if (!(await requireAdmin())) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = request.nextUrl + const type = searchParams.get('type') + const rawUrl = searchParams.get('url') ?? '' + const apiKey = searchParams.get('key') ?? undefined + const kind = searchParams.get('kind') ?? 'tags' + + if (!rawUrl) { + return NextResponse.json({ success: false, models: [], error: 'url parameter is required' }) + } + + const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/v1$/, '') + + try { + if (type === 'ollama') { + const res = await fetch(`${baseUrl}/api/tags`, { + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(5000), + }) + if (!res.ok) { + return NextResponse.json({ success: false, models: [], error: `Ollama ${res.status}` }) + } + const data = await res.json() + const models: string[] = (data.models ?? []).map((m: { name: string }) => m.name) + return NextResponse.json({ success: true, models }) + } + + if (type === 'custom') { + const headers: Record = { 'Content-Type': 'application/json' } + if (apiKey) headers['Authorization'] = `Bearer ${apiKey}` + + if (kind === 'embeddings') { + // Try provider-specific embeddings endpoint first (e.g. OpenRouter) + try { + const embRes = await fetch(`${baseUrl}/v1/embeddings/models`, { + headers, + signal: AbortSignal.timeout(8000), + }) + if (embRes.ok) { + const embData = await embRes.json() + const embModels: string[] = (embData.data ?? []) + .map((m: { id: string }) => m.id) + .filter(Boolean) + .sort() + if (embModels.length > 0) { + return NextResponse.json({ success: true, models: embModels }) + } + } + } catch { + // Fall through to /v1/models with keyword filter + } + } + + const res = await fetch(`${baseUrl}/v1/models`, { + headers, + signal: AbortSignal.timeout(8000), + }) + if (!res.ok) { + return NextResponse.json({ success: false, models: [], error: `Provider ${res.status}` }) + } + + const data = await res.json() + let models: string[] = (data.data ?? []) + .map((m: { id: string }) => m.id) + .filter(Boolean) + .sort() + + if (kind === 'embeddings') { + const keywords = ['embed', 'embedding', 'ada', 'e5', 'bge', 'gte', 'minilm'] + const filtered = models.filter((id) => + keywords.some((kw) => id.toLowerCase().includes(kw)) + ) + if (filtered.length > 0) models = filtered + } + + return NextResponse.json({ success: true, models }) + } + + return NextResponse.json({ success: false, models: [], error: `Unknown type: ${type}` }) + } catch (err: any) { + return NextResponse.json({ success: false, models: [], error: err.message ?? 'Request failed' }) + } +} diff --git a/memento-note/app/api/debug/client-error/route.ts b/memento-note/app/api/debug/client-error/route.ts new file mode 100644 index 0000000..55a5998 --- /dev/null +++ b/memento-note/app/api/debug/client-error/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + // Visible directement dans `docker logs memento-web` + console.error('[CLIENT-ERROR-REPORT]', JSON.stringify(body, null, 2)) + } catch { + // ignore + } + return NextResponse.json({ ok: true }) +} diff --git a/memento-note/app/layout.tsx b/memento-note/app/layout.tsx index 2a79e23..be06edf 100644 --- a/memento-note/app/layout.tsx +++ b/memento-note/app/layout.tsx @@ -7,6 +7,7 @@ import { getAISettings } from "@/app/actions/ai-settings"; import { getUserSettings } from "@/app/actions/user-settings"; import { ThemeInitializer } from "@/components/theme-initializer"; import { DirectionInitializer } from "@/components/direction-initializer"; +import { ErrorReporter } from "@/components/error-reporter"; import { auth } from "@/auth"; const inter = Inter({ @@ -29,7 +30,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#f59e0b", + themeColor: "#3A7CA5", }; function getHtmlClass(theme?: string): string { @@ -70,9 +71,12 @@ export default async function RootLayout({ return ( - + +