diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json new file mode 100644 index 0000000..ae941df --- /dev/null +++ b/.opencode/package-lock.json @@ -0,0 +1,376 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@opencode-ai/plugin": "1.4.9" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.9.tgz", + "integrity": "sha512-tUtPbPs5xP9wonwuz5d/2y8QTrqFR8HOtAVTXvZ6iG26NJfW0dnnw9oTusVOayEIemd5abytCESm7X9ZZOMftQ==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.4.9", + "effect": "4.0.0-beta.48", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.100", + "@opentui/solid": ">=0.1.100" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.9.tgz", + "integrity": "sha512-S8WQLuBFu2WwvSc1wupsV4qskniBA+JN1VaZZs52BPWwiN2zQFTD5/6dMh6oiYOMDtPjKsTFZ6qLFxDvVPNggQ==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.48", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", + "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", + "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz", + "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index ba1a63f..4b0ddf4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,27 @@ services: + # ============================================ + # PostgreSQL - Shared Database + # ============================================ + postgres: + image: postgres:16-alpine + container_name: memento-postgres + restart: unless-stopped + environment: + POSTGRES_USER: keepnotes + POSTGRES_PASSWORD: keepnotes + POSTGRES_DB: keepnotes + volumes: + - postgres-data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U keepnotes"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - memento-network + # ============================================ # keep-notes - Next.js Web Application # ============================================ @@ -12,7 +35,7 @@ services: ports: - "3000:3000" environment: - - DATABASE_URL=file:/app/prisma/dev.db + - DATABASE_URL=postgresql://keepnotes:keepnotes@postgres:5432/keepnotes - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-changethisinproduction} - NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000} - NODE_ENV=production @@ -34,8 +57,10 @@ services: - AI_MODEL_TAGS=${AI_MODEL_TAGS} - AI_MODEL_EMBEDDING=${AI_MODEL_EMBEDDING} volumes: - - db-data:/app/prisma - uploads-data:/app/public/uploads + depends_on: + postgres: + condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"] @@ -71,14 +96,11 @@ services: # Mode: 'stdio' (default, for Claude Desktop) or 'sse' (for HTTP/N8N) - MCP_MODE=${MCP_MODE:-stdio} - PORT=${MCP_PORT:-3001} - # Database path - must match the volume mount - - DATABASE_URL=file:/app/db/dev.db + # Database - connect to shared PostgreSQL + - DATABASE_URL=postgresql://keepnotes:keepnotes@postgres:5432/keepnotes - NODE_ENV=production - volumes: - # Shared database with keep-notes - - db-data:/app/db depends_on: - keep-notes: + postgres: condition: service_healthy restart: unless-stopped networks: @@ -119,7 +141,7 @@ services: # Volumes - Data Persistence # ============================================ volumes: - db-data: + postgres-data: driver: local uploads-data: driver: local diff --git a/keep-notes/.mcp.json b/keep-notes/.mcp.json new file mode 100644 index 0000000..3c88829 --- /dev/null +++ b/keep-notes/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "memento": { + "type": "http", + "url": "http://localhost:4242/mcp" + } + } +} diff --git a/keep-notes/app/(main)/admin/loading.tsx b/keep-notes/app/(main)/admin/loading.tsx new file mode 100644 index 0000000..4df6844 --- /dev/null +++ b/keep-notes/app/(main)/admin/loading.tsx @@ -0,0 +1,20 @@ +export default function AdminLoading() { + return ( +
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+ ))} +
+ ) +} diff --git a/keep-notes/app/(main)/layout.tsx b/keep-notes/app/(main)/layout.tsx index ef437ff..ecae9be 100644 --- a/keep-notes/app/(main)/layout.tsx +++ b/keep-notes/app/(main)/layout.tsx @@ -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 ( diff --git a/keep-notes/app/(main)/page.tsx b/keep-notes/app/(main)/page.tsx index 75054c3..d52946c 100644 --- a/keep-notes/app/(main)/page.tsx +++ b/keep-notes/app/(main)/page.tsx @@ -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 && ( setEditingNote({ note, readOnly })} diff --git a/keep-notes/app/(main)/settings/page.tsx b/keep-notes/app/(main)/settings/page.tsx index 6dfac31..8fd627b 100644 --- a/keep-notes/app/(main)/settings/page.tsx +++ b/keep-notes/app/(main)/settings/page.tsx @@ -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(null) - const [config, setConfig] = useState(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 ( - -
-
-

{t('settings.title')}

-

- {t('settings.description')} -

-
- - {/* Quick Links */} -
- -
- -

{t('aiSettings.title')}

-

- {t('aiSettings.description')} -

-
- - -
- -

{t('profile.title')}

-

- {t('profile.description')} -

-
- -
- - {/* AI Diagnostics */} - 🔍} - description={t('diagnostics.description')} - > -
-
-

{t('diagnostics.configuredProvider')}

-

{config?.provider || '...'}

-
-
-

{t('diagnostics.apiStatus')}

-
- {status === 'success' && } - {status === 'error' && } - - {status === 'success' ? t('diagnostics.operational') : - status === 'error' ? t('diagnostics.errorStatus') : - t('diagnostics.checking')} - -
-
-
- - {result && ( -
-

{t('diagnostics.testDetails')}

-
-
{JSON.stringify(result, null, 2)}
-
- - {status === 'error' && ( -
-

{t('diagnostics.troubleshootingTitle')}

-
    -
  • {t('diagnostics.tip1')}
  • -
  • {t('diagnostics.tip2')}
  • -
  • {t('diagnostics.tip3')}
  • -
  • {t('diagnostics.tip4')}
  • -
-
- )} -
- )} - -
- -
-
- - {/* Maintenance */} - 🔧} - description={t('settings.maintenanceDescription')} - > -
-
-
-

- {t('settings.cleanTags')} -

-

- {t('settings.cleanTagsDescription')} -

-
- -
- -
-
-

- {t('settings.semanticIndexing')} -

-

- {t('settings.semanticIndexingDescription')} -

-
- -
-
-
-
- ) +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') } diff --git a/keep-notes/app/(main)/settings/profile/page.tsx b/keep-notes/app/(main)/settings/profile/page.tsx index 565fb17..6b07ea5 100644 --- a/keep-notes/app/(main)/settings/profile/page.tsx +++ b/keep-notes/app/(main)/settings/profile/page.tsx @@ -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 ( diff --git a/keep-notes/app/actions/admin-settings.ts b/keep-notes/app/actions/admin-settings.ts index 5a311e2..9be51c4 100644 --- a/keep-notes/app/actions/admin-settings.ts +++ b/keep-notes/app/actions/admin-settings.ts @@ -3,6 +3,7 @@ import prisma from '@/lib/prisma' import { auth } from '@/auth' import { sendEmail } from '@/lib/mail' +import { revalidateTag } from 'next/cache' async function checkAdmin() { const session = await auth() @@ -29,11 +30,9 @@ export async function testSMTP() { export async function getSystemConfig() { await checkAdmin() - const configs = await prisma.systemConfig.findMany() - return configs.reduce((acc, conf) => { - acc[conf.key] = conf.value - return acc - }, {} as Record) + // Reuse the cached version from lib/config + const { getSystemConfig: getCachedConfig } = await import('@/lib/config') + return getCachedConfig() } export async function updateSystemConfig(data: Record) { @@ -45,8 +44,6 @@ export async function updateSystemConfig(data: Record) { Object.entries(data).filter(([key, value]) => value !== '' && value !== null && value !== undefined) ) - - const operations = Object.entries(filteredData).map(([key, value]) => prisma.systemConfig.upsert({ where: { key }, @@ -56,6 +53,10 @@ export async function updateSystemConfig(data: Record) { ) await prisma.$transaction(operations) + + // Invalidate cache after update + revalidateTag('system-config', '/settings') + return { success: true } } catch (error) { console.error('Failed to update settings:', error) diff --git a/keep-notes/app/actions/notes.ts b/keep-notes/app/actions/notes.ts index d54e48b..9e3568a 100644 --- a/keep-notes/app/actions/notes.ts +++ b/keep-notes/app/actions/notes.ts @@ -410,7 +410,6 @@ export async function createNote(data: { isMarkdown: data.isMarkdown || false, size: data.size || 'small', embedding: null, // Generated in background - sharedWith: data.sharedWith && data.sharedWith.length > 0 ? JSON.stringify(data.sharedWith) : null, autoGenerated: data.autoGenerated || null, notebookId: data.notebookId || null, } diff --git a/keep-notes/app/api/admin/embeddings/validate/route.ts b/keep-notes/app/api/admin/embeddings/validate/route.ts index 026ee07..1532039 100644 --- a/keep-notes/app/api/admin/embeddings/validate/route.ts +++ b/keep-notes/app/api/admin/embeddings/validate/route.ts @@ -55,9 +55,10 @@ export async function GET() { continue } - // Parse and validate embedding + // Validate embedding try { - const embedding = JSON.parse(note.embedding) + if (!note.embedding) continue + const embedding = JSON.parse(note.embedding) as number[] const validation = validateEmbedding(embedding) if (!validation.valid) { diff --git a/keep-notes/app/api/admin/sync-labels/route.ts b/keep-notes/app/api/admin/sync-labels/route.ts index 0d99c22..f4a17c7 100644 --- a/keep-notes/app/api/admin/sync-labels/route.ts +++ b/keep-notes/app/api/admin/sync-labels/route.ts @@ -15,9 +15,8 @@ export async function GET() { notes.forEach((note: any) => { if (note.labels) { try { - const parsed = JSON.parse(note.labels); - if (Array.isArray(parsed)) { - parsed.forEach((l: string) => uniqueLabels.add(l)); + if (Array.isArray(note.labels)) { + (note.labels as string[]).forEach((l: string) => uniqueLabels.add(l)); } } catch (e) { // ignore error diff --git a/keep-notes/app/api/fix-labels/route.ts b/keep-notes/app/api/fix-labels/route.ts index 81e3690..1d38c42 100644 --- a/keep-notes/app/api/fix-labels/route.ts +++ b/keep-notes/app/api/fix-labels/route.ts @@ -34,7 +34,7 @@ export async function POST() { allNotes.forEach(note => { if (note.labels) { try { - const parsed: string[] = JSON.parse(note.labels) + const parsed: string[] = Array.isArray(note.labels) ? (note.labels as string[]) : [] if (Array.isArray(parsed)) { parsed.forEach(l => { if (l && l.trim()) labelsInNotes.add(l.trim()) @@ -81,7 +81,7 @@ export async function POST() { allNotes.forEach(note => { if (note.labels) { try { - const parsed: string[] = JSON.parse(note.labels) + const parsed: string[] = Array.isArray(note.labels) ? (note.labels as string[]) : [] if (Array.isArray(parsed)) { parsed.forEach(l => usedLabelsSet.add(l.toLowerCase())) } diff --git a/keep-notes/app/api/labels/[id]/route.ts b/keep-notes/app/api/labels/[id]/route.ts index 409756a..7bea138 100644 --- a/keep-notes/app/api/labels/[id]/route.ts +++ b/keep-notes/app/api/labels/[id]/route.ts @@ -114,7 +114,7 @@ export async function PUT( for (const note of allNotes) { if (note.labels) { try { - const noteLabels: string[] = JSON.parse(note.labels) + const noteLabels: string[] = Array.isArray(note.labels) ? (note.labels as string[]) : [] const updatedLabels = noteLabels.map(l => l.toLowerCase() === currentLabel.name.toLowerCase() ? newName : l ) @@ -123,7 +123,7 @@ export async function PUT( await prisma.note.update({ where: { id: note.id }, data: { - labels: JSON.stringify(updatedLabels) + labels: updatedLabels as any } }) } @@ -211,7 +211,7 @@ export async function DELETE( for (const note of allNotes) { if (note.labels) { try { - const noteLabels: string[] = JSON.parse(note.labels) + const noteLabels: string[] = Array.isArray(note.labels) ? (note.labels as string[]) : [] const filteredLabels = noteLabels.filter( l => l.toLowerCase() !== label.name.toLowerCase() ) @@ -220,7 +220,7 @@ export async function DELETE( await prisma.note.update({ where: { id: note.id }, data: { - labels: filteredLabels.length > 0 ? JSON.stringify(filteredLabels) : null + labels: (filteredLabels.length > 0 ? filteredLabels : null) as any } }) } diff --git a/keep-notes/app/api/notebooks/route.ts b/keep-notes/app/api/notebooks/route.ts index cd54f40..ccf0b08 100644 --- a/keep-notes/app/api/notebooks/route.ts +++ b/keep-notes/app/api/notebooks/route.ts @@ -21,7 +21,7 @@ export async function GET(request: NextRequest) { orderBy: { name: 'asc' } }, _count: { - select: { notes: true } + select: { notes: { where: { isArchived: false } } } } }, orderBy: { order: 'asc' } @@ -82,7 +82,7 @@ export async function POST(request: NextRequest) { include: { labels: true, _count: { - select: { notes: true } + select: { notes: { where: { isArchived: false } } } } } }) diff --git a/keep-notes/app/api/notes/[id]/move/route.ts b/keep-notes/app/api/notes/[id]/move/route.ts index fcae75f..b8cf4ae 100644 --- a/keep-notes/app/api/notes/[id]/move/route.ts +++ b/keep-notes/app/api/notes/[id]/move/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { auth } from '@/auth' -import { revalidatePath } from 'next/cache' import { reconcileLabelsAfterNoteMove } from '@/app/actions/notes' // POST /api/notes/[id]/move - Move a note to a notebook (or to Inbox) @@ -77,7 +76,9 @@ export async function POST( await reconcileLabelsAfterNoteMove(id, targetNotebookId) - revalidatePath('/') + // No revalidatePath('/') here — the client-side triggerRefresh() in + // notebooks-context.tsx handles the refresh. Avoiding server-side + // revalidation prevents a double-refresh (server + client). return NextResponse.json({ success: true, diff --git a/keep-notes/app/api/notes/[id]/route.ts b/keep-notes/app/api/notes/[id]/route.ts index c6b9b4d..8e89e20 100644 --- a/keep-notes/app/api/notes/[id]/route.ts +++ b/keep-notes/app/api/notes/[id]/route.ts @@ -87,10 +87,10 @@ export async function PUT( const updateData: any = { ...body } if ('checkItems' in body) { - updateData.checkItems = body.checkItems ? JSON.stringify(body.checkItems) : null + updateData.checkItems = body.checkItems ?? null } if ('labels' in body) { - updateData.labels = body.labels ? JSON.stringify(body.labels) : null + updateData.labels = body.labels ?? null } updateData.updatedAt = new Date() diff --git a/keep-notes/app/api/notes/route.ts b/keep-notes/app/api/notes/route.ts index 33f8d3b..c84bc60 100644 --- a/keep-notes/app/api/notes/route.ts +++ b/keep-notes/app/api/notes/route.ts @@ -83,9 +83,9 @@ export async function POST(request: NextRequest) { content: content || '', color: color || 'default', type: type || 'text', - checkItems: checkItems ? JSON.stringify(checkItems) : null, - labels: labels ? JSON.stringify(labels) : null, - images: images ? JSON.stringify(images) : null, + checkItems: checkItems ?? null, + labels: labels ?? null, + images: images ?? null, } }) @@ -147,11 +147,11 @@ export async function PUT(request: NextRequest) { if (content !== undefined) updateData.content = content if (color !== undefined) updateData.color = color if (type !== undefined) updateData.type = type - if (checkItems !== undefined) updateData.checkItems = checkItems ? JSON.stringify(checkItems) : null - if (labels !== undefined) updateData.labels = labels ? JSON.stringify(labels) : null + if (checkItems !== undefined) updateData.checkItems = checkItems ?? null + if (labels !== undefined) updateData.labels = labels ?? null if (isPinned !== undefined) updateData.isPinned = isPinned if (isArchived !== undefined) updateData.isArchived = isArchived - if (images !== undefined) updateData.images = images ? JSON.stringify(images) : null + if (images !== undefined) updateData.images = images ?? null const note = await prisma.note.update({ where: { id }, diff --git a/keep-notes/app/layout.tsx b/keep-notes/app/layout.tsx index 6888933..19b186f 100644 --- a/keep-notes/app/layout.tsx +++ b/keep-notes/app/layout.tsx @@ -6,7 +6,6 @@ import { SessionProviderWrapper } from "@/components/session-provider-wrapper"; import { getAISettings } from "@/app/actions/ai-settings"; import { getUserSettings } from "@/app/actions/user-settings"; import { ThemeInitializer } from "@/components/theme-initializer"; -import { getThemeScript } from "@/lib/theme-script"; import { auth } from "@/auth"; const inter = Inter({ @@ -32,6 +31,12 @@ export const viewport: Viewport = { themeColor: "#f59e0b", }; +function getHtmlClass(theme?: string): string { + if (theme === 'dark') return 'dark'; + if (theme === 'midnight') return 'dark'; + return ''; +} + export default async function RootLayout({ children, }: Readonly<{ @@ -46,16 +51,9 @@ export default async function RootLayout({ getUserSettings(userId) ]) - - return ( - + -