fix: resolve React Error #310 and refactor admin section
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled

- Fix React bug #33580: remove Suspense boundaries co-located with Link components
- Delete settings/loading.tsx and admin/loading.tsx (root cause of race condition)
- Convert all admin navigation from Next.js Link to anchor tags
- Move admin pages to dedicated (admin) route group
- Add AdminHeader matching main header visual design
- Add AdminSidebar with anchor-based navigation
- Add /api/admin/models route handler (replaces server actions for GET)
- Add /api/debug/client-error for server-side browser error reporting
- Add useNoteRefreshOptional() to fix crash in AdminHeader
- Hide Admin Dashboard menu for non-admin users
- Change app icons from yellow to blue (#3A7CA5) matching brand primary
- Fix admin search bar width to match main header

Made-with: Cursor
This commit is contained in:
2026-04-25 20:46:10 +02:00
parent 1d53c16cc2
commit 986d438738
49 changed files with 1277 additions and 358 deletions

View File

@@ -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 *)"
]
}
}

9
.kilo/agent-manager.json Normal file
View File

@@ -0,0 +1,9 @@
{
"worktrees": {},
"sessions": {},
"tabOrder": {
"local": [
"pending:1"
]
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="flex justify-between items-center mb-8">
<div className="flex items-center gap-3">
<Link href="/admin/settings">
<a href="/admin/settings">
<Button variant="outline" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
</a>
<div>
<h1 className="text-3xl font-bold flex items-center gap-2">
<TestTube className="h-8 w-8" />

View File

@@ -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
</p>
</div>
<Link href="/admin/settings">
<a href="/admin/settings">
<Button variant="outline">
<Settings className="mr-2 h-4 w-4" />
Configure
</Button>
</Link>
</a>
</div>
<AdminMetrics metrics={aiMetrics} />

View File

@@ -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 (
<div className="mx-auto max-w-2xl rounded-lg border border-red-200 bg-red-50 p-6 text-red-900 dark:border-red-900 dark:bg-red-950/30 dark:text-red-100">
<h2 className="text-lg font-semibold">Une erreur est survenue dans l'administration</h2>
<p className="mt-2 text-sm opacity-90">
Le rendu de cette page a echoue. Vous pouvez reessayer sans recharger toute l'application.
</p>
<div className="mt-4">
<Button type="button" onClick={reset}>
Reessayer
</Button>
</div>
</div>
)
}

View File

@@ -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 <a> 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 (
<div className="bg-background-light dark:bg-background-dark font-display text-slate-900 dark:text-white overflow-hidden h-screen flex flex-col">
<AdminHeader />
<div className="flex flex-1 overflow-hidden">
<AdminSidebar />
<AdminContentArea>{children}</AdminContentArea>
</div>
</div>
)
}

View File

@@ -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<string, string> }
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<string, string> }
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<string, string> }
}
}, [])
// 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<string, string> }
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<string, string> }
}
}, [])
// 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(() => {
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 || ''
if (tagsProvider === 'ollama') {
const url = config.OLLAMA_BASE_URL_TAGS || config.OLLAMA_BASE_URL || 'http://localhost:11434'
fetchOllamaModels('tags', url)
await fetchOllamaModels('tags', config.OLLAMA_BASE_URL_TAGS || ollamaBase)
} else if (tagsProvider === 'custom' && customUrl) {
await fetchCustomModels('tags', customUrl, customKey)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tagsProvider])
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)
await fetchOllamaModels('embeddings', config.OLLAMA_BASE_URL_EMBEDDING || ollamaBase)
} else if (embeddingsProvider === 'custom' && customUrl) {
await fetchCustomModels('embeddings', customUrl, customKey)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [embeddingsProvider])
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])
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)
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<string, string> }
</CardContent>
<CardFooter className="flex justify-between pt-6">
<Button type="submit" disabled={isSaving}>{isSaving ? t('admin.ai.saving') : t('admin.ai.saveSettings')}</Button>
<Link href="/admin/ai-test">
<a href="/admin/ai-test">
<Button type="button" variant="outline" className="gap-2">
<TestTube className="h-4 w-4" />
{t('admin.ai.openTestPanel')}
<ExternalLink className="h-3 w-3" />
</Button>
</Link>
</a>
</CardFooter>
</form>
</Card>

View File

@@ -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 <Suspense> here intentionally: combining a Suspense boundary with <Link>
// 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 (
<AdminProvidersWrapper
initialLanguage={initialLanguage}
initialTranslations={initialTranslations}
>
{children}
</AdminProvidersWrapper>
)
}

View File

@@ -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 (
<div className="flex h-full bg-gray-50 dark:bg-zinc-950">
<AdminSidebar />
<AdminContentArea>{children}</AdminContentArea>
</div>
)
}

View File

@@ -1,20 +0,0 @@
export default function AdminLoading() {
return (
<div className="space-y-6 animate-pulse">
<div>
<div className="h-9 w-48 bg-muted rounded-md mb-2" />
<div className="h-4 w-72 bg-muted rounded-md" />
</div>
{[1, 2, 3].map((i) => (
<div key={i} className="rounded-lg border border-border bg-white dark:bg-zinc-900 p-6 space-y-4">
<div className="h-5 w-40 bg-muted rounded" />
<div className="h-px bg-border" />
<div className="space-y-3">
<div className="h-4 w-full bg-muted rounded" />
<div className="h-4 w-3/4 bg-muted rounded" />
</div>
</div>
))}
</div>
)
}

View File

@@ -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 */}
<div className="flex flex-1 overflow-hidden">
{/* Sidebar Navigation - Style Keep */}
<Suspense fallback={<div className="w-64 flex-none hidden md:flex" />}>
<Sidebar className="w-64 flex-none flex-col bg-white dark:bg-[#1e2128] border-e border-slate-200 dark:border-slate-800 overflow-y-auto hidden md:flex" user={session?.user} />
</Suspense>
{/* Main Content Area */}
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-background-light dark:bg-background-dark p-4 scroll-smooth">

View File

@@ -1,26 +0,0 @@
export default function SettingsLoading() {
return (
<div className="space-y-6 animate-pulse">
<div>
<div className="h-9 w-64 bg-muted rounded-md mb-2" />
<div className="h-4 w-96 bg-muted rounded-md" />
</div>
{[1, 2, 3].map((i) => (
<div key={i} className="rounded-lg border border-border p-6 space-y-4">
<div className="flex items-center gap-3">
<div className="h-8 w-8 bg-muted rounded-full" />
<div className="h-5 w-40 bg-muted rounded-md" />
</div>
<div className="h-px bg-border" />
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/30">
<div className="space-y-2">
<div className="h-4 w-32 bg-muted rounded" />
<div className="h-3 w-56 bg-muted rounded" />
</div>
<div className="h-6 w-11 bg-muted rounded-full" />
</div>
</div>
))}
</div>
)
}

View File

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

View File

@@ -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=<base_url>
* GET /api/admin/models?type=custom&url=<base_url>&key=<api_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<string, string> = { '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' })
}
}

View File

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

View File

@@ -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 (
<html suppressHydrationWarning className={getHtmlClass(userSettings.theme)}>
<head />
<head>
<script dangerouslySetInnerHTML={{ __html: `if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(r){r.unregister()})})}` }} />
</head>
<body className={inter.className}>
<SessionProviderWrapper>
<ErrorReporter />
<DirectionInitializer />
<ThemeInitializer theme={userSettings.theme} fontSize={aiSettings.fontSize} />
{children}

View File

@@ -0,0 +1,141 @@
'use client'
import { signOut, useSession } from 'next-auth/react'
import { Shield, Search, Settings, LogOut, User, StickyNote, MessageSquare, FlaskConical, Bot } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { NotificationPanel } from './notification-panel'
import { useLanguage } from '@/lib/i18n'
/**
* Admin header — visuellement identique au Header principal.
* Utilise exclusivement des <a> (rechargement complet) au lieu de <Link>
* pour éviter React Error #310 (bug React #33580 / Next.js #63388).
*/
export function AdminHeader() {
const { data: session } = useSession()
const { t } = useLanguage()
const user = session?.user
const initial = user?.name
? user.name.charAt(0).toUpperCase()
: user?.email?.[0]?.toUpperCase() ?? '?'
return (
<header className="flex-none flex items-center justify-between whitespace-nowrap border-b border-solid border-slate-200 dark:border-slate-800 bg-white dark:bg-[#1e2128] px-6 py-3 z-20">
{/* ── Logo + Search ── */}
<div className="flex items-center gap-8">
<a href="/" className="flex items-center gap-3 text-slate-900 dark:text-white group no-underline">
<div className="size-8 bg-primary rounded-lg flex items-center justify-center text-primary-foreground shadow-sm group-hover:shadow-md transition-all">
<StickyNote className="w-5 h-5" />
</div>
<h2 className="text-xl font-bold leading-tight tracking-tight">MEMENTO</h2>
</a>
{/* Badge Admin */}
<span className="hidden sm:flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-primary/10 text-primary text-xs font-semibold">
<Shield className="h-3 w-3" />
Admin
</span>
{/* Search (décoratif en mode admin) — même taille que l'entête principale */}
<label className="hidden md:flex flex-col min-w-40 w-96 !h-10">
<div className="flex w-full flex-1 items-stretch rounded-full h-full bg-slate-100 dark:bg-slate-800 border border-transparent">
<div className="text-slate-400 dark:text-slate-400 flex items-center justify-center pl-4">
<Search className="w-4 h-4" />
</div>
<input
className="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden bg-transparent border-none text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 text-sm focus:ring-0 focus:outline-none"
placeholder={t('search.placeholder') || 'Rechercher…'}
type="text"
disabled
aria-label="Recherche désactivée en mode admin"
/>
</div>
</label>
</div>
{/* ── Droite : nav + notifs + settings + avatar ── */}
<div className="flex flex-1 justify-end gap-2 items-center">
{/* Nav pills — toutes en <a> pour éviter la RSC race condition */}
<div className="hidden md:flex items-center gap-1 bg-slate-100 dark:bg-slate-800/60 rounded-full px-1.5 py-1">
<a
href="/chat"
className="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<MessageSquare className="h-3.5 w-3.5" />
<span>{t('nav.chat') || 'AI Chat'}</span>
</a>
<a
href="/agents"
className="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<Bot className="h-3.5 w-3.5" />
<span>{t('nav.agents') || 'Agents'}</span>
</a>
<a
href="/lab"
className="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<FlaskConical className="h-3.5 w-3.5" />
<span>{t('nav.lab') || 'The Lab'}</span>
</a>
</div>
{/* Notifications */}
<NotificationPanel />
{/* Settings */}
<a
href="/settings"
className="flex items-center justify-center size-10 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 transition-colors"
aria-label="Paramètres"
>
<Settings className="w-5 h-5" />
</a>
{/* Avatar + menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className="flex items-center justify-center bg-center bg-no-repeat bg-cover rounded-full size-10 ring-2 ring-white dark:ring-slate-700 cursor-pointer shadow-sm hover:shadow-md transition-shadow bg-primary/10 dark:bg-primary/20 text-primary dark:text-primary-foreground"
style={user?.image ? { backgroundImage: `url(${(user as any).image})` } : undefined}
>
{!user?.image && (
<span className="text-sm font-semibold">{initial}</span>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="flex items-center justify-start gap-2 p-2">
<div className="flex flex-col space-y-1 leading-none">
{user?.name && <p className="font-medium">{user.name}</p>}
{user?.email && <p className="w-[200px] truncate text-sm text-muted-foreground">{user.email}</p>}
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem asChild className="cursor-pointer">
<a href="/settings/profile">
<User className="mr-2 h-4 w-4" />
<span>{t('settings.profile') || 'Profile'}</span>
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => signOut({ callbackUrl: '/' })}
className="cursor-pointer text-red-600 focus:text-red-600"
>
<LogOut className="mr-2 h-4 w-4" />
<span>{t('auth.signOut') || 'Se déconnecter'}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
)
}

View File

@@ -0,0 +1,26 @@
'use client'
import { LanguageProvider } from '@/lib/i18n/LanguageProvider'
import type { Translations } from '@/lib/i18n/load-translations'
import type { ReactNode } from 'react'
interface AdminProvidersWrapperProps {
children: ReactNode
initialLanguage?: string
initialTranslations?: Translations
}
export function AdminProvidersWrapper({
children,
initialLanguage = 'en',
initialTranslations,
}: AdminProvidersWrapperProps) {
return (
<LanguageProvider
initialLanguage={initialLanguage as any}
initialTranslations={initialTranslations}
>
{children}
</LanguageProvider>
)
}

View File

@@ -1,6 +1,5 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { LayoutDashboard, Users, Brain, Settings } from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -55,7 +54,10 @@ export function AdminSidebar({ className }: AdminSidebarProps) {
const isActive = pathname === item.href || (item.href !== '/admin' && pathname?.startsWith(item.href))
return (
<Link
// <a> instead of <Link>: avoids Next.js RSC navigation transitions
// that trigger React Error #310 (React bug #33580) in production.
// Full-page reloads are acceptable for admin navigation.
<a
key={item.href}
href={item.href}
className={cn(
@@ -68,7 +70,7 @@ export function AdminSidebar({ className }: AdminSidebarProps) {
>
{item.icon}
<span>{t(item.titleKey)}</span>
</Link>
</a>
)
})}
</nav>

View File

@@ -0,0 +1,53 @@
'use client'
import { useEffect } from 'react'
/**
* Captures unhandled client-side errors and forwards them to the server
* so they appear in `docker logs memento-web`. Loaded in the root layout.
*/
export function ErrorReporter() {
useEffect(() => {
function report(payload: object) {
fetch('/api/debug/client-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true,
}).catch(() => {})
}
function onError(event: ErrorEvent) {
report({
type: 'uncaught-error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack ?? null,
url: window.location.href,
timestamp: new Date().toISOString(),
})
}
function onUnhandledRejection(event: PromiseRejectionEvent) {
const reason = event.reason
report({
type: 'unhandled-rejection',
message: reason?.message ?? String(reason),
stack: reason?.stack ?? null,
url: window.location.href,
timestamp: new Date().toISOString(),
})
}
window.addEventListener('error', onError)
window.addEventListener('unhandledrejection', onUnhandledRejection)
return () => {
window.removeEventListener('error', onError)
window.removeEventListener('unhandledrejection', onUnhandledRejection)
}
}, [])
return null
}

View File

@@ -58,6 +58,7 @@ export function Header({
const noSidebarMode = ['/agents', '/chat', '/lab'].some(r => pathname.startsWith(r))
// Track last pushed search to avoid infinite loops
const lastPushedSearch = useRef<string | null>(null)
@@ -421,12 +422,16 @@ export function Header({
<span>{t('settings.profile') || 'Profile'}</span>
</Link>
</DropdownMenuItem>
{(currentUser as any)?.role === 'ADMIN' && (
<DropdownMenuItem asChild className="cursor-pointer">
<Link href="/admin">
{/* Force hard reload: client-side navigation between (main) and (admin)
route groups triggers React #310 in Next.js 16.x (framework bug). */}
<a href="/admin">
<Shield className="mr-2 h-4 w-4" />
<span>{t('nav.adminDashboard')}</span>
</Link>
</a>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => signOut()} className="cursor-pointer text-red-600 focus:text-red-600">
<LogOut className="mr-2 h-4 w-4" />
<span>{t('auth.signOut') || 'Sign out'}</span>

View File

@@ -11,7 +11,7 @@ import {
} from '@/components/ui/popover'
import { getPendingShareRequests, respondToShareRequest } from '@/app/actions/notes'
import { toast } from 'sonner'
import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
@@ -36,7 +36,7 @@ interface ShareRequest {
}
export function NotificationPanel() {
const { triggerRefresh } = useNoteRefresh()
const { triggerRefresh } = useNoteRefreshOptional()
const { t } = useLanguage()
const [requests, setRequests] = useState<ShareRequest[]>([])
const [isLoading, setIsLoading] = useState(false)

View File

@@ -26,7 +26,7 @@ import { useHomeViewOptional } from '@/context/home-view-context'
import { useEffect, useState } from 'react'
import { getTrashCount } from '@/app/actions/notes'
const HIDDEN_ROUTES = ['/agents', '/chat', '/lab']
const HIDDEN_ROUTES = ['/agents', '/chat', '/lab', '/admin']
export function Sidebar({ className, user }: { className?: string, user?: any }) {
const pathname = usePathname()
@@ -36,10 +36,15 @@ export function Sidebar({ className, user }: { className?: string, user?: any })
const homeBridge = useHomeViewOptional()
const [trashCount, setTrashCount] = useState(0)
// Fetch trash count
const searchKey = searchParams.toString()
// Fetch trash count — skip for hidden/admin routes to avoid dispatching a
// Server Action during an ongoing App Router navigation transition, which
// would increment React's nested-update counter and trigger Error #310.
useEffect(() => {
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return
getTrashCount().then(setTrashCount)
}, [pathname, searchParams])
}, [pathname, searchKey])
// Hide sidebar on Agents, Chat IA and Lab routes
if (HIDDEN_ROUTES.some(r => pathname.startsWith(r))) return null

View File

@@ -1,6 +1,6 @@
'use client'
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode } from 'react'
import { LabelColorName, LABEL_COLORS } from '@/lib/types'
import { getHashColor } from '@/lib/utils'
@@ -32,13 +32,12 @@ export function LabelProvider({ children }: { children: ReactNode }) {
const [loading, setLoading] = useState(true)
const [notebookId, setNotebookId] = useState<string | null>(null)
const fetchLabels = async () => {
const fetchLabels = useCallback(async (nbId: string | null) => {
try {
setLoading(true)
// Build URL with notebookId filter
const url = new URL('/api/labels', window.location.origin)
if (notebookId) {
url.searchParams.set('notebookId', notebookId)
if (nbId) {
url.searchParams.set('notebookId', nbId)
}
const response = await fetch(url.toString(), {
@@ -54,14 +53,13 @@ export function LabelProvider({ children }: { children: ReactNode }) {
} finally {
setLoading(false)
}
}
}, [])
// Re-fetch labels when notebookId changes
useEffect(() => {
fetchLabels()
}, [notebookId])
fetchLabels(notebookId)
}, [notebookId, fetchLabels])
const addLabel = async (name: string, color?: LabelColorName, labelNotebookId?: string | null) => {
const addLabel = useCallback(async (name: string, color?: LabelColorName, labelNotebookId?: string | null) => {
try {
const labelColor = color || getHashColor(name);
const finalNotebookId = labelNotebookId || notebookId
@@ -79,9 +77,9 @@ export function LabelProvider({ children }: { children: ReactNode }) {
console.error('Failed to add label:', error)
throw error
}
}
}, [notebookId])
const updateLabel = async (id: string, updates: Partial<Pick<Label, 'name' | 'color'>>) => {
const updateLabel = useCallback(async (id: string, updates: Partial<Pick<Label, 'name' | 'color'>>) => {
try {
const response = await fetch(`/api/labels/${id}`, {
method: 'PUT',
@@ -98,9 +96,9 @@ export function LabelProvider({ children }: { children: ReactNode }) {
console.error('Failed to update label:', error)
throw error
}
}
}, [])
const deleteLabel = async (id: string) => {
const deleteLabel = useCallback(async (id: string) => {
try {
const response = await fetch(`/api/labels/${id}`, {
method: 'DELETE',
@@ -112,18 +110,18 @@ export function LabelProvider({ children }: { children: ReactNode }) {
console.error('Failed to delete label:', error)
throw error
}
}
}, [])
const getLabelColorHelper = (name: string): LabelColorName => {
const getLabelColor = useCallback((name: string): LabelColorName => {
const label = labels.find(l => l.name.toLowerCase() === name.toLowerCase())
return label?.color || 'gray'
}
}, [labels])
const refreshLabels = async () => {
await fetchLabels()
}
const refreshLabels = useCallback(async () => {
await fetchLabels(notebookId)
}, [fetchLabels, notebookId])
const value: LabelContextType = {
const value = useMemo<LabelContextType>(() => ({
labels,
loading,
notebookId,
@@ -131,9 +129,9 @@ export function LabelProvider({ children }: { children: ReactNode }) {
addLabel,
updateLabel,
deleteLabel,
getLabelColor: getLabelColorHelper,
getLabelColor,
refreshLabels,
}
}), [labels, loading, notebookId, addLabel, updateLabel, deleteLabel, getLabelColor, refreshLabels])
return <LabelContext.Provider value={value}>{children}</LabelContext.Provider>
}

View File

@@ -1,6 +1,6 @@
'use client'
import { createContext, useContext, useState, useCallback } from 'react'
import { createContext, useContext, useState, useCallback, useMemo } from 'react'
interface NoteRefreshContextType {
refreshKey: number
@@ -16,8 +16,10 @@ export function NoteRefreshProvider({ children }: { children: React.ReactNode })
setRefreshKey(prev => prev + 1)
}, [])
const value = useMemo(() => ({ refreshKey, triggerRefresh }), [refreshKey, triggerRefresh])
return (
<NoteRefreshContext.Provider value={{ refreshKey, triggerRefresh }}>
<NoteRefreshContext.Provider value={value}>
{children}
</NoteRefreshContext.Provider>
)
@@ -30,3 +32,12 @@ export function useNoteRefresh() {
}
return context
}
/**
* Same as useNoteRefresh but tolerates being called outside the provider
* (e.g. shared header rendered in admin pages). Returns a no-op when absent.
*/
export function useNoteRefreshOptional(): NoteRefreshContextType {
const context = useContext(NoteRefreshContext)
return context ?? { refreshKey: 0, triggerRefresh: () => {} }
}

View File

@@ -1,6 +1,6 @@
'use client'
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react'
interface NotebookDragContextValue {
draggedNoteId: string | null
@@ -46,9 +46,7 @@ export function NotebookDragProvider({ children }: NotebookDragProviderProps) {
const isDragging = draggedNoteId !== null
const isDragOver = dragOverNotebookId !== null
return (
<NotebookDragContext.Provider
value={{
const value = useMemo(() => ({
draggedNoteId,
dragOverNotebookId,
startDrag,
@@ -56,8 +54,10 @@ export function NotebookDragProvider({ children }: NotebookDragProviderProps) {
dragOver,
isDragging,
isDragOver,
}}
>
}), [draggedNoteId, dragOverNotebookId, startDrag, endDrag, dragOver, isDragging, isDragOver])
return (
<NotebookDragContext.Provider value={value}>
{children}
</NotebookDragContext.Provider>
)

View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
echo "Running Prisma migrations..."
node ./node_modules/prisma/build/index.js migrate deploy
echo "Starting server..."
exec node server.js

View File

@@ -1,16 +1,36 @@
import prisma from './prisma'
// Environment variable fallbacks for system config keys
const ENV_FALLBACKS: Record<string, string> = {
AI_PROVIDER_TAGS: process.env.AI_PROVIDER_TAGS || '',
AI_MODEL_TAGS: process.env.AI_MODEL_TAGS || '',
AI_PROVIDER_EMBEDDING: process.env.AI_PROVIDER_EMBEDDING || '',
AI_MODEL_EMBEDDING: process.env.AI_MODEL_EMBEDDING || '',
AI_PROVIDER_CHAT: process.env.AI_PROVIDER_CHAT || '',
AI_MODEL_CHAT: process.env.AI_MODEL_CHAT || '',
OLLAMA_BASE_URL: process.env.OLLAMA_BASE_URL || '',
OPENAI_API_KEY: process.env.OPENAI_API_KEY || '',
CUSTOM_OPENAI_API_KEY: process.env.CUSTOM_OPENAI_API_KEY || '',
CUSTOM_OPENAI_BASE_URL: process.env.CUSTOM_OPENAI_BASE_URL || '',
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION || '',
NEXTAUTH_URL: process.env.NEXTAUTH_URL || '',
}
export async function getSystemConfig() {
let dbConfig: Record<string, string> = {}
try {
const configs = await prisma.systemConfig.findMany()
return configs.reduce((acc, conf) => {
dbConfig = configs.reduce((acc, conf) => {
acc[conf.key] = conf.value
return acc
}, {} as Record<string, string>)
} catch (e) {
console.error('Failed to load system config from DB:', e)
return {}
}
// Merge: DB values take precedence, env vars as fallback
const merged = { ...ENV_FALLBACKS, ...dbConfig }
return merged
}
/**

View File

@@ -1,6 +1,6 @@
'use client'
import { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react'
import { createContext, useContext, useEffect, useState, useCallback, useRef, useMemo } from 'react'
import type { ReactNode } from 'react'
import { SupportedLanguage, loadTranslations, getTranslationValue, Translations } from './load-translations'
@@ -97,8 +97,10 @@ export function LanguageProvider({ children, initialLanguage = 'en', initialTran
return typeof value === 'string' ? value : key
}, [translations])
const value = useMemo(() => ({ language, setLanguage, t, translations }), [language, setLanguage, t, translations])
return (
<LanguageContext.Provider value={{ language, setLanguage, t, translations }}>
<LanguageContext.Provider value={value}>
{children}
</LanguageContext.Provider>
)

View File

@@ -4,9 +4,6 @@ const nextConfig: NextConfig = {
// Enable standalone output for Docker
output: 'standalone',
// Optimize for production
reactStrictMode: true,
// Image optimization (enabled for better performance)
images: {
formats: ['image/avif', 'image/webp'],
@@ -15,7 +12,12 @@ const nextConfig: NextConfig = {
// Hide the "compiling" indicator
devIndicators: false,
turbopack: {},
// Disable strict mode: React 19 strict mode can cause double-invocation of render
// functions during concurrent transitions, amplifying timing issues.
reactStrictMode: false,
// TEMP: disable Turbopack due React #310 loop on /admin routes in production builds.
// We keep webpack pipeline until upstream fix is confirmed.
};
export default nextConfig;

View File

@@ -16,6 +16,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@excalidraw/excalidraw": "^0.18.0",
"@mozilla/readability": "^0.6.0",
"@prisma/client": "^5.22.0",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
@@ -54,6 +55,7 @@
"remark-math": "^6.0.0",
"resend": "^6.12.0",
"rss-parser": "^3.13.0",
"sharp": "^0.34.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tinyld": "^1.3.4",
@@ -62,7 +64,6 @@
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@prisma/client": "^5.22.0",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@types/bcryptjs": "^2.4.6",
@@ -474,6 +475,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -520,6 +522,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -541,6 +544,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -577,28 +581,6 @@
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -1606,7 +1588,6 @@
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
@@ -1694,9 +1675,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1713,9 +1691,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1732,9 +1707,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1751,9 +1723,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1770,9 +1739,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1789,9 +1755,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1808,9 +1771,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1827,9 +1787,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1846,9 +1803,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1871,9 +1825,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1896,9 +1847,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1921,9 +1869,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1946,9 +1891,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1971,9 +1913,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1996,9 +1935,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2021,9 +1957,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2247,9 +2180,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2266,9 +2196,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2285,9 +2212,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2304,9 +2228,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2376,12 +2297,337 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright": "1.59.1"
},
@@ -2398,6 +2644,7 @@
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=16.13"
},
@@ -5360,9 +5607,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5380,9 +5624,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5400,9 +5641,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5420,9 +5658,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5440,9 +5675,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5460,9 +5692,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5508,6 +5737,18 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
@@ -5714,9 +5955,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5734,9 +5972,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5754,9 +5989,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -5774,9 +6006,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -6268,6 +6497,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -6278,6 +6508,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -6332,6 +6563,7 @@
"integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.4",
@@ -6671,6 +6903,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -6842,6 +7075,7 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -7077,6 +7311,7 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz",
"integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
@@ -7486,6 +7721,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -7676,7 +7912,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -8540,6 +8775,7 @@
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz",
"integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.20.0"
},
@@ -8838,9 +9074,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -8862,9 +9095,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -8886,9 +9116,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -8910,9 +9137,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9455,6 +9679,7 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz",
"integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "12.0.0",
"@chevrotain/gast": "12.0.0",
@@ -10256,6 +10481,14 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/node-releases": {
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
@@ -10267,6 +10500,7 @@
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
"integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
"license": "MIT-0",
"peer": true,
"engines": {
"node": ">=6.0.0"
}
@@ -10586,6 +10820,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -10620,6 +10855,7 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -10641,6 +10877,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/engines": "5.22.0"
},
@@ -10658,6 +10895,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -10863,6 +11101,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10872,6 +11111,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -11265,7 +11505,6 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -11280,7 +11519,6 @@
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
@@ -11537,7 +11775,8 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.3.tgz",
"integrity": "sha512-fA/NX5gMf0ooCLISgB0wScaWgaj6rjTN2SVAwleURjiya7ITNkV+VMmoHtKkldP6CIZoYCZyxb8zP/e2TWoEtQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tapable": {
"version": "2.3.2",
@@ -11622,6 +11861,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12104,6 +12344,7 @@
"integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.1.4",
"@vitest/mocker": "4.1.4",
@@ -12215,6 +12456,23 @@
}
}
},
"node_modules/vitest/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/vitest/node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -12230,6 +12488,14 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/vitest/node_modules/immutable": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
@@ -12243,12 +12509,28 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vitest/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/vitest/node_modules/vite": {
"version": "8.0.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -12517,6 +12799,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -71,14 +71,15 @@
"resend": "^6.12.0",
"rss-parser": "^3.13.0",
"sonner": "^2.0.7",
"sharp": "^0.34.0",
"tailwind-merge": "^3.4.0",
"tinyld": "^1.3.4",
"vazirmatn": "^33.0.3",
"zod": "^4.3.5"
"zod": "^4.3.5",
"@prisma/client": "^5.22.0"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@prisma/client": "^5.22.0",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@types/bcryptjs": "^2.4.6",

View File

@@ -0,0 +1,182 @@
-- AlterTable: User
ALTER TABLE "User" ADD COLUMN "cardSizeMode" TEXT NOT NULL DEFAULT 'variable';
-- AlterTable: Note
ALTER TABLE "Note" ADD COLUMN "trashedAt" TIMESTAMP(3);
ALTER TABLE "Note" ALTER COLUMN "checkItems" DROP DEFAULT;
ALTER TABLE "Note" ALTER COLUMN "checkItems" TYPE TEXT USING "checkItems"::TEXT;
ALTER TABLE "Note" ALTER COLUMN "labels" DROP DEFAULT;
ALTER TABLE "Note" ALTER COLUMN "labels" TYPE TEXT USING "labels"::TEXT;
ALTER TABLE "Note" ALTER COLUMN "images" DROP DEFAULT;
ALTER TABLE "Note" ALTER COLUMN "images" TYPE TEXT USING "images"::TEXT;
ALTER TABLE "Note" ALTER COLUMN "links" DROP DEFAULT;
ALTER TABLE "Note" ALTER COLUMN "links" TYPE TEXT USING "links"::TEXT;
ALTER TABLE "Note" ALTER COLUMN "sharedWith" DROP DEFAULT;
ALTER TABLE "Note" ALTER COLUMN "sharedWith" TYPE TEXT USING "sharedWith"::TEXT;
ALTER TABLE "Note" DROP COLUMN "embedding";
-- AlterTable: AiFeedback
ALTER TABLE "AiFeedback" ALTER COLUMN "metadata" DROP DEFAULT;
ALTER TABLE "AiFeedback" ALTER COLUMN "metadata" TYPE TEXT USING "metadata"::TEXT;
-- CreateTable: NoteEmbedding
CREATE TABLE "NoteEmbedding" (
"id" TEXT NOT NULL,
"noteId" TEXT NOT NULL,
"embedding" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NoteEmbedding_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "NoteEmbedding_noteId_key" ON "NoteEmbedding"("noteId");
CREATE INDEX "NoteEmbedding_noteId_idx" ON "NoteEmbedding"("noteId");
-- CreateTable: Agent
CREATE TABLE "Agent" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"type" TEXT DEFAULT 'scraper',
"role" TEXT NOT NULL,
"sourceUrls" TEXT,
"frequency" TEXT NOT NULL DEFAULT 'manual',
"lastRun" TIMESTAMP(3),
"nextRun" TIMESTAMP(3),
"isEnabled" BOOLEAN NOT NULL DEFAULT true,
"targetNotebookId" TEXT,
"sourceNotebookId" TEXT,
"tools" TEXT DEFAULT '[]',
"maxSteps" INTEGER NOT NULL DEFAULT 10,
"notifyEmail" BOOLEAN NOT NULL DEFAULT false,
"includeImages" BOOLEAN NOT NULL DEFAULT false,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Agent_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Agent_userId_idx" ON "Agent"("userId");
CREATE INDEX "Agent_isEnabled_idx" ON "Agent"("isEnabled");
-- CreateTable: AgentAction
CREATE TABLE "AgentAction" (
"id" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"result" TEXT,
"log" TEXT,
"input" TEXT,
"toolLog" TEXT,
"tokensUsed" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AgentAction_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "AgentAction_agentId_idx" ON "AgentAction"("agentId");
-- CreateTable: Conversation
CREATE TABLE "Conversation" (
"id" TEXT NOT NULL,
"title" TEXT,
"userId" TEXT NOT NULL,
"notebookId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Conversation_userId_idx" ON "Conversation"("userId");
CREATE INDEX "Conversation_notebookId_idx" ON "Conversation"("notebookId");
-- CreateTable: ChatMessage
CREATE TABLE "ChatMessage" (
"id" TEXT NOT NULL,
"conversationId" TEXT NOT NULL,
"role" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ChatMessage_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ChatMessage_conversationId_idx" ON "ChatMessage"("conversationId");
-- CreateTable: Canvas
CREATE TABLE "Canvas" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"data" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Canvas_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Canvas_userId_idx" ON "Canvas"("userId");
-- CreateTable: Workflow
CREATE TABLE "Workflow" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"graph" TEXT NOT NULL DEFAULT '{"nodes":[],"edges":[]}',
"isEnabled" BOOLEAN NOT NULL DEFAULT true,
"userId" TEXT NOT NULL,
"notebookId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Workflow_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Workflow_userId_idx" ON "Workflow"("userId");
CREATE INDEX "Workflow_isEnabled_idx" ON "Workflow"("isEnabled");
-- CreateTable: WorkflowRun
CREATE TABLE "WorkflowRun" (
"id" TEXT NOT NULL,
"workflowId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'running',
"log" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WorkflowRun_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "WorkflowRun_workflowId_idx" ON "WorkflowRun"("workflowId");
CREATE INDEX "WorkflowRun_status_idx" ON "WorkflowRun"("status");
-- AddForeignKey: NoteEmbedding
ALTER TABLE "NoteEmbedding" ADD CONSTRAINT "NoteEmbedding_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Agent
ALTER TABLE "Agent" ADD CONSTRAINT "Agent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Agent" ADD CONSTRAINT "Agent_targetNotebookId_fkey" FOREIGN KEY ("targetNotebookId") REFERENCES "Notebook"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey: AgentAction
ALTER TABLE "AgentAction" ADD CONSTRAINT "AgentAction_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Conversation
ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_notebookId_fkey" FOREIGN KEY ("notebookId") REFERENCES "Notebook"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey: ChatMessage
ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Canvas
ALTER TABLE "Canvas" ADD CONSTRAINT "Canvas_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Workflow
ALTER TABLE "Workflow" ADD CONSTRAINT "Workflow_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Workflow" ADD CONSTRAINT "Workflow_notebookId_fkey" FOREIGN KEY ("notebookId") REFERENCES "Notebook"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey: WorkflowRun
ALTER TABLE "WorkflowRun" ADD CONSTRAINT "WorkflowRun_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "Workflow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Add missing Note index
CREATE INDEX "Note_trashedAt_idx" ON "Note"("trashedAt");

View File

@@ -1,6 +1,6 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["debian-openssl-1.1.x", "native"]
binaryTargets = ["debian-openssl-3.0.x", "native"]
}
datasource db {

View File

@@ -1,4 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192">
<rect width="192" height="192" fill="#f59e0b" rx="40"/>
<path d="M50 50 h92 v92 h-92 z" fill="#fff"/>
<rect width="192" height="192" fill="#3A7CA5" rx="40"/>
<g transform="translate(96,96)">
<rect x="-34" y="-34" width="68" height="68" rx="5" fill="white"/>
<path d="M34,34 L34,11 L11,34 Z" fill="#3A7CA5" opacity="0.3"/>
<line x1="-23" y1="-17" x2="23" y2="-17" stroke="#e5e7eb" stroke-width="3" stroke-linecap="round"/>
<line x1="-23" y1="-4" x2="23" y2="-4" stroke="#e5e7eb" stroke-width="3" stroke-linecap="round"/>
<line x1="-23" y1="9" x2="11" y2="9" stroke="#e5e7eb" stroke-width="3" stroke-linecap="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 615 B

View File

@@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" fill="#f59e0b" rx="100"/>
<path d="M150 150 h212 v212 h-212 z" fill="#fff"/>
<path d="M150 150 h212" stroke="#fff3cd" stroke-width="20"/>
<rect width="512" height="512" fill="#3A7CA5" rx="100"/>
<g transform="translate(256,256)">
<rect x="-90" y="-90" width="180" height="180" rx="12" fill="white"/>
<path d="M90,90 L90,30 L30,90 Z" fill="#3A7CA5" opacity="0.3"/>
<line x1="-60" y1="-45" x2="60" y2="-45" stroke="#e5e7eb" stroke-width="8" stroke-linecap="round"/>
<line x1="-60" y1="-10" x2="60" y2="-10" stroke="#e5e7eb" stroke-width="8" stroke-linecap="round"/>
<line x1="-60" y1="25" x2="30" y2="25" stroke="#e5e7eb" stroke-width="8" stroke-linecap="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 625 B

View File

@@ -5,7 +5,7 @@
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#f59e0b",
"theme_color": "#3A7CA5",
"icons": [
{
"src": "/icons/icon-192.svg",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long