chore: snapshot before performance optimization

This commit is contained in:
Sepehr Ramezani
2026-04-17 21:14:43 +02:00
parent b6a548acd8
commit 2eceb32fd4
95 changed files with 4357 additions and 1942 deletions

View File

@@ -0,0 +1,20 @@
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

@@ -9,8 +9,11 @@ export default async function MainLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const session = await auth();
const initialLanguage = await detectUserLanguage();
// Run auth + language detection in parallel
const [session, initialLanguage] = await Promise.all([
auth(),
detectUserLanguage(),
]);
return (
<ProvidersWrapper initialLanguage={initialLanguage}>

View File

@@ -449,7 +449,8 @@ export default function HomePage() {
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
/>
{!isTabs && showRecentNotes && (
{/* Recent notes section hidden in masonry mode — notes are already visible in the grid below */}
{false && !isTabs && showRecentNotes && (
<RecentNotesSection
recentNotes={recentNotes}
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}

View File

@@ -1,238 +1,7 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import { SettingsSection } from '@/components/settings'
import { Button } from '@/components/ui/button'
import { Loader2, CheckCircle, XCircle, RefreshCw, Database, BrainCircuit } from 'lucide-react'
import { cleanupAllOrphans, syncAllEmbeddings } from '@/app/actions/notes'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
import Link from 'next/link'
import { useLabels } from '@/context/LabelContext'
import { useNotebooks } from '@/context/notebooks-context'
import { useNoteRefresh } from '@/context/NoteRefreshContext'
export default function SettingsPage() {
const { t } = useLanguage()
const router = useRouter()
const { refreshLabels } = useLabels()
const { refreshNotebooks } = useNotebooks()
const { triggerRefresh } = useNoteRefresh()
const [loading, setLoading] = useState(false)
const [cleanupLoading, setCleanupLoading] = useState(false)
const [syncLoading, setSyncLoading] = useState(false)
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle')
const [result, setResult] = useState<any>(null)
const [config, setConfig] = useState<any>(null)
const checkConnection = async () => {
setLoading(true)
setStatus('idle')
setResult(null)
try {
const res = await fetch('/api/ai/test')
const data = await res.json()
setConfig({
provider: data.provider,
status: res.ok ? 'connected' : 'disconnected'
})
if (res.ok) {
setStatus('success')
setResult(data)
} else {
setStatus('error')
setResult(data)
}
} catch (error: any) {
console.error(error)
setStatus('error')
setResult({ message: error.message, stack: error.stack })
} finally {
setLoading(false)
}
}
const handleSync = async () => {
setSyncLoading(true)
try {
const result = await syncAllEmbeddings()
if (result.success) {
toast.success(t('settings.indexingComplete', { count: result.count ?? 0 }))
triggerRefresh()
router.refresh()
}
} catch (error) {
console.error(error)
toast.error(t('settings.indexingError'))
} finally {
setSyncLoading(false)
}
}
const handleCleanup = async () => {
setCleanupLoading(true)
try {
const result = await cleanupAllOrphans()
if (result.success) {
const errCount = Array.isArray(result.errors) ? result.errors.length : 0
if (result.created === 0 && result.deleted === 0 && errCount === 0) {
toast.info(t('settings.cleanupNothing'))
} else {
const base = t('settings.cleanupDone', {
created: result.created ?? 0,
deleted: result.deleted ?? 0,
})
toast.success(errCount > 0 ? `${base} (${t('settings.cleanupWithErrors')})` : base)
}
await refreshLabels()
await refreshNotebooks()
triggerRefresh()
router.refresh()
} else {
toast.error(t('settings.cleanupError'))
}
} catch (error) {
console.error(error)
toast.error(t('settings.cleanupError'))
} finally {
setCleanupLoading(false)
}
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">{t('settings.title')}</h1>
<p className="text-muted-foreground">
{t('settings.description')}
</p>
</div>
{/* Quick Links */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link href="/settings/ai">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<BrainCircuit className="h-6 w-6 text-purple-500 mb-2" />
<h3 className="font-semibold">{t('aiSettings.title')}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('aiSettings.description')}
</p>
</div>
</Link>
<Link href="/settings/profile">
<div className="p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
<RefreshCw className="h-6 w-6 text-primary mb-2" />
<h3 className="font-semibold">{t('profile.title')}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('profile.description')}
</p>
</div>
</Link>
</div>
{/* AI Diagnostics */}
<SettingsSection
title={t('diagnostics.title')}
icon={<span className="text-2xl">🔍</span>}
description={t('diagnostics.description')}
>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">{t('diagnostics.configuredProvider')}</p>
<p className="text-lg font-mono">{config?.provider || '...'}</p>
</div>
<div className="p-4 rounded-lg bg-secondary/50">
<p className="text-sm font-medium text-muted-foreground mb-1">{t('diagnostics.apiStatus')}</p>
<div className="flex items-center gap-2">
{status === 'success' && <CheckCircle className="text-green-500 w-5 h-5" />}
{status === 'error' && <XCircle className="text-red-500 w-5 h-5" />}
<span className={`text-sm font-medium ${status === 'success' ? 'text-green-600 dark:text-green-400' :
status === 'error' ? 'text-red-600 dark:text-red-400' :
'text-gray-600'
}`}>
{status === 'success' ? t('diagnostics.operational') :
status === 'error' ? t('diagnostics.errorStatus') :
t('diagnostics.checking')}
</span>
</div>
</div>
</div>
{result && (
<div className="space-y-2 mt-4">
<h3 className="text-sm font-medium">{t('diagnostics.testDetails')}</h3>
<div className={`p-4 rounded-md font-mono text-xs overflow-x-auto ${status === 'error'
? 'bg-red-50 text-red-900 border border-red-200 dark:bg-red-950 dark:text-red-100 dark:border-red-900'
: 'bg-slate-950 text-slate-50'
}`}>
<pre>{JSON.stringify(result, null, 2)}</pre>
</div>
{status === 'error' && (
<div className="text-sm text-red-600 dark:text-red-400 mt-2">
<p className="font-bold">{t('diagnostics.troubleshootingTitle')}</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>{t('diagnostics.tip1')}</li>
<li>{t('diagnostics.tip2')}</li>
<li>{t('diagnostics.tip3')}</li>
<li>{t('diagnostics.tip4')}</li>
</ul>
</div>
)}
</div>
)}
<div className="mt-4 flex justify-end">
<Button variant="outline" size="sm" onClick={checkConnection} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
{t('general.testConnection')}
</Button>
</div>
</SettingsSection>
{/* Maintenance */}
<SettingsSection
title={t('settings.maintenance')}
icon={<span className="text-2xl">🔧</span>}
description={t('settings.maintenanceDescription')}
>
<div className="space-y-4 py-4">
<div className="flex items-center justify-between gap-4 p-4 border border-border rounded-lg bg-card">
<div className="min-w-0">
<h3 className="font-medium flex items-center gap-2">
{t('settings.cleanTags')}
</h3>
<p className="text-sm text-muted-foreground">
{t('settings.cleanTagsDescription')}
</p>
</div>
<Button variant="secondary" onClick={handleCleanup} disabled={cleanupLoading} className="shrink-0">
{cleanupLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Database className="w-4 h-4 mr-2" />}
{t('general.clean')}
</Button>
</div>
<div className="flex items-center justify-between gap-4 p-4 border border-border rounded-lg bg-card">
<div className="min-w-0">
<h3 className="font-medium flex items-center gap-2">
{t('settings.semanticIndexing')}
</h3>
<p className="text-sm text-muted-foreground">
{t('settings.semanticIndexingDescription')}
</p>
</div>
<Button variant="secondary" onClick={handleSync} disabled={syncLoading} className="shrink-0">
{syncLoading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <BrainCircuit className="w-4 h-4 mr-2" />}
{t('general.indexAll')}
</Button>
</div>
</div>
</SettingsSection>
</div>
)
import { redirect } from 'next/navigation'
// Immediate redirect to the first settings sub-page
// This avoids loading the heavy settings/page.tsx client component on first visit
export default function SettingsIndexPage() {
redirect('/settings/general')
}

View File

@@ -2,8 +2,6 @@ import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { ProfileForm } from './profile-form'
import prisma from '@/lib/prisma'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Sparkles } from 'lucide-react'
import { ProfilePageHeader } from '@/components/profile-page-header'
import { AISettingsLinkCard } from './ai-settings-link-card'
@@ -14,30 +12,24 @@ export default async function ProfilePage() {
redirect('/login')
}
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { name: true, email: true, role: true }
})
// Parallel queries
const [user, aiSettings] = await Promise.all([
prisma.user.findUnique({
where: { id: session.user.id },
select: { name: true, email: true, role: true }
}),
prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
])
if (!user) {
redirect('/login')
}
// Get user AI settings
let userAISettings = { preferredLanguage: 'auto', showRecentNotes: false }
try {
const aiSettings = await prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
if (aiSettings) {
userAISettings = {
preferredLanguage: aiSettings.preferredLanguage || 'auto',
showRecentNotes: aiSettings.showRecentNotes ?? false
}
}
} catch (error) {
console.error('Error fetching AI settings:', error)
const userAISettings = {
preferredLanguage: aiSettings?.preferredLanguage || 'auto',
showRecentNotes: aiSettings?.showRecentNotes ?? false
}
return (