diff --git a/.svelte-kit/ambient.d.ts b/.svelte-kit/ambient.d.ts index edfa9e71..222545c0 100644 --- a/.svelte-kit/ambient.d.ts +++ b/.svelte-kit/ambient.d.ts @@ -26,113 +26,145 @@ * ``` */ declare module '$env/static/private' { - export const WORKSPACE_BASE: string; - export const GITHUB_TOKEN: string; - export const KUBERNETES_SERVICE_PORT: string; - export const TMUX: string; - export const POETRY_VIRTUALENVS_PATH: string; - export const KUBERNETES_PORT: string; - export const MAIL: string; - export const SERVE_FRONTEND: string; - export const USER: string; - export const npm_config_user_agent: string; - export const GIT_EDITOR: string; - export const PERMITTED_CORS_ORIGINS: string; - export const HOSTNAME: string; - export const OPENVSCODE_SERVER_ROOT: string; - export const npm_node_execpath: string; - export const SHLVL: string; - export const ALLOW_SET_CONVERSATION_ID: string; - export const npm_config_noproxy: string; - export const HOME: string; - export const CONDA_SHLVL: string; - export const INIT_GIT_IN_EMPTY_WORKSPACE: string; - export const OLDPWD: string; - export const TERM_PROGRAM_VERSION: string; - export const OPENHANDS_REPO_PATH: string; - export const npm_package_json: string; - export const GPG_KEY: string; - export const PS1: string; - export const POETRY_HOME: string; + export const VITE_API_URL: string; + export const NODE_ENV: string; + export const DOMAIN: string; + export const FRONTEND_URL: string; + export const BACKEND_URL: string; + export const DB_PASSWORD: string; + export const REDIS_PASSWORD: string; + export const JWT_SECRET: string; + export const SESSION_SECRET: string; + export const CSRF_SECRET: string; + export const SMTP_HOST: string; + export const SMTP_PORT: string; + export const SMTP_SECURE: string; + export const SMTP_USER: string; + export const SMTP_PASS: string; + export const FROM_EMAIL: string; + export const FROM_NAME: string; + export const STORAGE_TYPE: string; + export const AWS_ACCESS_KEY_ID: string; + export const AWS_SECRET_ACCESS_KEY: string; + export const AWS_REGION: string; + export const AWS_S3_BUCKET: string; + export const GOOGLE_CLIENT_ID: string; + export const GOOGLE_CLIENT_SECRET: string; + export const GITHUB_CLIENT_ID: string; + export const GITHUB_CLIENT_SECRET: string; + export const DISCORD_CLIENT_ID: string; + export const DISCORD_CLIENT_SECRET: string; + export const SLACK_BOT_TOKEN: string; + export const SLACK_SIGNING_SECRET: string; + export const SLACK_WEBHOOK_URL: string; + export const DISCORD_BOT_TOKEN: string; + export const DISCORD_WEBHOOK_URL: string; + export const GOOGLE_CALENDAR_ENABLED: string; + export const OUTLOOK_CLIENT_ID: string; + export const OUTLOOK_CLIENT_SECRET: string; + export const GOOGLE_DRIVE_ENABLED: string; + export const DROPBOX_CLIENT_ID: string; + export const DROPBOX_CLIENT_SECRET: string; + export const GITHUB_INTEGRATION_ENABLED: string; + export const GITHUB_WEBHOOK_SECRET: string; + export const GITLAB_CLIENT_ID: string; + export const GITLAB_CLIENT_SECRET: string; + export const WEBHOOKS_ENABLED: string; + export const PROMETHEUS_ENABLED: string; + export const GRAFANA_USER: string; + export const GRAFANA_PASSWORD: string; + export const SENTRY_DSN: string; + export const BACKUP_RETENTION_DAYS: string; + export const BACKUP_SCHEDULE: string; + export const BACKUP_S3_BUCKET: string; + export const SSL_EMAIL: string; + export const SHELL: string; + export const npm_command: string; export const npm_config_userconfig: string; - export const npm_config_local_prefix: string; - export const PS2: string; - export const PYTHON_SHA256: string; - export const VISUAL: string; + export const COLORTERM: string; + export const HYPRLAND_CMD: string; + export const npm_config_cache: string; + export const XDG_SESSION_PATH: string; + export const GLFW_IM_MODULE: string; + export const XDG_MENU_PREFIX: string; + export const XDG_BACKEND: string; + export const NODE: string; + export const LC_ADDRESS: string; + export const ILLOGICAL_IMPULSE_VIRTUAL_ENV: string; + export const LC_NAME: string; + export const INPUT_METHOD: string; export const COLOR: string; - export const RUNTIME_URL: string; + export const npm_config_local_prefix: string; + export const XMODIFIERS: string; + export const DESKTOP_SESSION: string; + export const LC_MONETARY: string; + export const ELECTRON_OZONE_PLATFORM_HINT: string; + export const KITTY_PID: string; + export const npm_config_globalconfig: string; + export const EDITOR: string; + export const XDG_SEAT: string; + export const PWD: string; + export const XDG_SESSION_DESKTOP: string; + export const LOGNAME: string; + export const QT_QPA_PLATFORMTHEME: string; + export const XDG_SESSION_TYPE: string; + export const npm_config_init_module: string; export const _: string; - export const npm_config_prefix: string; + export const KITTY_PUBLIC_KEY: string; + export const TERMINAL: string; + export const MOTD_SHOWN: string; + export const HOME: string; + export const LANG: string; + export const LC_PAPER: string; + export const _JAVA_AWT_WM_NONREPARENTING: string; + export const XDG_CURRENT_DESKTOP: string; + export const npm_package_version: string; + export const STARSHIP_SHELL: string; + export const WAYLAND_DISPLAY: string; + export const KITTY_WINDOW_ID: string; + export const FORCE_COLOR: string; + export const XDG_SEAT_PATH: string; + export const INIT_CWD: string; + export const STARSHIP_SESSION_KEY: string; + export const QT_QPA_PLATFORM: string; + export const npm_lifecycle_script: string; + export const SDL_IM_MODULE: string; export const npm_config_npm_version: string; - export const SANDBOX_CLOSE_DELAY: string; - export const WORK_PORT_1: string; - export const LOG_JSON: string; + export const XDG_SESSION_CLASS: string; + export const LC_IDENTIFICATION: string; export const TERM: string; - export const WORK_PORT_2: string; - export const npm_config_cache: string; - export const KUBERNETES_PORT_443_TCP_ADDR: string; - export const WORKSPACE_MOUNT_PATH_IN_SANDBOX: string; - export const npm_config_node_gyp: string; - export const PATH: string; - export const SESSION_API_KEY: string; - export const NODE: string; + export const TERMINFO: string; export const npm_package_name: string; - export const port: string; - export const OR_SITE_URL: string; - export const KUBERNETES_PORT_443_TCP_PORT: string; - export const MAMBA_ROOT_PREFIX: string; - export const OR_APP_NAME: string; - export const SDL_AUDIODRIVER: string; - export const KUBERNETES_PORT_443_TCP_PROTO: string; - export const TIKTOKEN_CACHE_DIR: string; - export const LANG: string; - export const FILE_STORE_WEB_HOOK_BATCH: string; - export const CONVERSATION_MANAGER_CLASS: string; - export const VSCODE_PORT: string; - export const TERM_PROGRAM: string; - export const npm_lifecycle_script: string; - export const GSETTINGS_SCHEMA_DIR: string; - export const SHELL: string; - export const RUNTIME: string; - export const npm_package_version: string; + export const npm_config_prefix: string; + export const USER: string; + export const HYPRLAND_INSTANCE_SIGNATURE: string; + export const DISPLAY: string; export const npm_lifecycle_event: string; - export const PYTHON_VERSION: string; - export const SKIP_DEPENDENCY_CHECK: string; - export const PROMPT_COMMAND: string; - export const OH_INTERPRETER_PATH: string; - export const CONDA_DEFAULT_ENV: string; - export const FILE_STORE_WEB_HOOK_URL: string; - export const KUBERNETES_SERVICE_PORT_HTTPS: string; - export const KUBERNETES_PORT_443_TCP: string; - export const MAMBA_EXE: string; - export const VIRTUAL_ENV: string; - export const npm_config_globalconfig: string; - export const npm_config_init_module: string; - export const PWD: string; - export const KUBERNETES_SERVICE_HOST: string; - export const LC_ALL: string; + export const GSK_RENDERER: string; + export const SHLVL: string; + export const MOZ_ENABLE_WAYLAND: string; + export const LC_TELEPHONE: string; + export const QT_IM_MODULE: string; + export const LC_MEASUREMENT: string; + export const XDG_VTNR: string; + export const XDG_SESSION_ID: string; + export const npm_config_user_agent: string; export const npm_execpath: string; - export const FILE_STORE_PATH: string; + export const XDG_RUNTIME_DIR: string; + export const DEBUGINFOD_URLS: string; + export const npm_package_json: string; + export const LC_TIME: string; + export const npm_config_noproxy: string; + export const BROWSER: string; + export const PATH: string; + export const npm_config_node_gyp: string; + export const DBUS_SESSION_BUS_ADDRESS: string; export const npm_config_global_prefix: string; - export const PYTHONPATH: string; - export const npm_command: string; - export const CONDA_PREFIX: string; - export const GSETTINGS_SCHEMA_DIR_CONDA_BACKUP: string; - export const LOCAL_RUNTIME_MODE: string; - export const LOG_JSON_LEVEL_KEY: string; - export const TMUX_PANE: string; - export const EDITOR: string; - export const WEB_HOST: string; - export const INITIAL_NUM_WARM_SERVERS: string; - export const PYGAME_HIDE_SUPPORT_PROMPT: string; - export const INIT_CWD: string; - export const TEST: string; - export const VITEST: string; - export const NODE_ENV: string; - export const PROD: string; - export const DEV: string; - export const BASE_URL: string; - export const MODE: string; + export const MAIL: string; + export const KITTY_INSTALLATION_DIR: string; + export const npm_node_execpath: string; + export const LC_NUMERIC: string; + export const VITE_USER_NODE_ENV: string; } /** @@ -162,113 +194,145 @@ declare module '$env/static/public' { */ declare module '$env/dynamic/private' { export const env: { - WORKSPACE_BASE: string; - GITHUB_TOKEN: string; - KUBERNETES_SERVICE_PORT: string; - TMUX: string; - POETRY_VIRTUALENVS_PATH: string; - KUBERNETES_PORT: string; - MAIL: string; - SERVE_FRONTEND: string; - USER: string; - npm_config_user_agent: string; - GIT_EDITOR: string; - PERMITTED_CORS_ORIGINS: string; - HOSTNAME: string; - OPENVSCODE_SERVER_ROOT: string; - npm_node_execpath: string; - SHLVL: string; - ALLOW_SET_CONVERSATION_ID: string; - npm_config_noproxy: string; - HOME: string; - CONDA_SHLVL: string; - INIT_GIT_IN_EMPTY_WORKSPACE: string; - OLDPWD: string; - TERM_PROGRAM_VERSION: string; - OPENHANDS_REPO_PATH: string; - npm_package_json: string; - GPG_KEY: string; - PS1: string; - POETRY_HOME: string; + VITE_API_URL: string; + NODE_ENV: string; + DOMAIN: string; + FRONTEND_URL: string; + BACKEND_URL: string; + DB_PASSWORD: string; + REDIS_PASSWORD: string; + JWT_SECRET: string; + SESSION_SECRET: string; + CSRF_SECRET: string; + SMTP_HOST: string; + SMTP_PORT: string; + SMTP_SECURE: string; + SMTP_USER: string; + SMTP_PASS: string; + FROM_EMAIL: string; + FROM_NAME: string; + STORAGE_TYPE: string; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + AWS_REGION: string; + AWS_S3_BUCKET: string; + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + GITHUB_CLIENT_ID: string; + GITHUB_CLIENT_SECRET: string; + DISCORD_CLIENT_ID: string; + DISCORD_CLIENT_SECRET: string; + SLACK_BOT_TOKEN: string; + SLACK_SIGNING_SECRET: string; + SLACK_WEBHOOK_URL: string; + DISCORD_BOT_TOKEN: string; + DISCORD_WEBHOOK_URL: string; + GOOGLE_CALENDAR_ENABLED: string; + OUTLOOK_CLIENT_ID: string; + OUTLOOK_CLIENT_SECRET: string; + GOOGLE_DRIVE_ENABLED: string; + DROPBOX_CLIENT_ID: string; + DROPBOX_CLIENT_SECRET: string; + GITHUB_INTEGRATION_ENABLED: string; + GITHUB_WEBHOOK_SECRET: string; + GITLAB_CLIENT_ID: string; + GITLAB_CLIENT_SECRET: string; + WEBHOOKS_ENABLED: string; + PROMETHEUS_ENABLED: string; + GRAFANA_USER: string; + GRAFANA_PASSWORD: string; + SENTRY_DSN: string; + BACKUP_RETENTION_DAYS: string; + BACKUP_SCHEDULE: string; + BACKUP_S3_BUCKET: string; + SSL_EMAIL: string; + SHELL: string; + npm_command: string; npm_config_userconfig: string; - npm_config_local_prefix: string; - PS2: string; - PYTHON_SHA256: string; - VISUAL: string; + COLORTERM: string; + HYPRLAND_CMD: string; + npm_config_cache: string; + XDG_SESSION_PATH: string; + GLFW_IM_MODULE: string; + XDG_MENU_PREFIX: string; + XDG_BACKEND: string; + NODE: string; + LC_ADDRESS: string; + ILLOGICAL_IMPULSE_VIRTUAL_ENV: string; + LC_NAME: string; + INPUT_METHOD: string; COLOR: string; - RUNTIME_URL: string; + npm_config_local_prefix: string; + XMODIFIERS: string; + DESKTOP_SESSION: string; + LC_MONETARY: string; + ELECTRON_OZONE_PLATFORM_HINT: string; + KITTY_PID: string; + npm_config_globalconfig: string; + EDITOR: string; + XDG_SEAT: string; + PWD: string; + XDG_SESSION_DESKTOP: string; + LOGNAME: string; + QT_QPA_PLATFORMTHEME: string; + XDG_SESSION_TYPE: string; + npm_config_init_module: string; _: string; - npm_config_prefix: string; + KITTY_PUBLIC_KEY: string; + TERMINAL: string; + MOTD_SHOWN: string; + HOME: string; + LANG: string; + LC_PAPER: string; + _JAVA_AWT_WM_NONREPARENTING: string; + XDG_CURRENT_DESKTOP: string; + npm_package_version: string; + STARSHIP_SHELL: string; + WAYLAND_DISPLAY: string; + KITTY_WINDOW_ID: string; + FORCE_COLOR: string; + XDG_SEAT_PATH: string; + INIT_CWD: string; + STARSHIP_SESSION_KEY: string; + QT_QPA_PLATFORM: string; + npm_lifecycle_script: string; + SDL_IM_MODULE: string; npm_config_npm_version: string; - SANDBOX_CLOSE_DELAY: string; - WORK_PORT_1: string; - LOG_JSON: string; + XDG_SESSION_CLASS: string; + LC_IDENTIFICATION: string; TERM: string; - WORK_PORT_2: string; - npm_config_cache: string; - KUBERNETES_PORT_443_TCP_ADDR: string; - WORKSPACE_MOUNT_PATH_IN_SANDBOX: string; - npm_config_node_gyp: string; - PATH: string; - SESSION_API_KEY: string; - NODE: string; + TERMINFO: string; npm_package_name: string; - port: string; - OR_SITE_URL: string; - KUBERNETES_PORT_443_TCP_PORT: string; - MAMBA_ROOT_PREFIX: string; - OR_APP_NAME: string; - SDL_AUDIODRIVER: string; - KUBERNETES_PORT_443_TCP_PROTO: string; - TIKTOKEN_CACHE_DIR: string; - LANG: string; - FILE_STORE_WEB_HOOK_BATCH: string; - CONVERSATION_MANAGER_CLASS: string; - VSCODE_PORT: string; - TERM_PROGRAM: string; - npm_lifecycle_script: string; - GSETTINGS_SCHEMA_DIR: string; - SHELL: string; - RUNTIME: string; - npm_package_version: string; + npm_config_prefix: string; + USER: string; + HYPRLAND_INSTANCE_SIGNATURE: string; + DISPLAY: string; npm_lifecycle_event: string; - PYTHON_VERSION: string; - SKIP_DEPENDENCY_CHECK: string; - PROMPT_COMMAND: string; - OH_INTERPRETER_PATH: string; - CONDA_DEFAULT_ENV: string; - FILE_STORE_WEB_HOOK_URL: string; - KUBERNETES_SERVICE_PORT_HTTPS: string; - KUBERNETES_PORT_443_TCP: string; - MAMBA_EXE: string; - VIRTUAL_ENV: string; - npm_config_globalconfig: string; - npm_config_init_module: string; - PWD: string; - KUBERNETES_SERVICE_HOST: string; - LC_ALL: string; + GSK_RENDERER: string; + SHLVL: string; + MOZ_ENABLE_WAYLAND: string; + LC_TELEPHONE: string; + QT_IM_MODULE: string; + LC_MEASUREMENT: string; + XDG_VTNR: string; + XDG_SESSION_ID: string; + npm_config_user_agent: string; npm_execpath: string; - FILE_STORE_PATH: string; + XDG_RUNTIME_DIR: string; + DEBUGINFOD_URLS: string; + npm_package_json: string; + LC_TIME: string; + npm_config_noproxy: string; + BROWSER: string; + PATH: string; + npm_config_node_gyp: string; + DBUS_SESSION_BUS_ADDRESS: string; npm_config_global_prefix: string; - PYTHONPATH: string; - npm_command: string; - CONDA_PREFIX: string; - GSETTINGS_SCHEMA_DIR_CONDA_BACKUP: string; - LOCAL_RUNTIME_MODE: string; - LOG_JSON_LEVEL_KEY: string; - TMUX_PANE: string; - EDITOR: string; - WEB_HOST: string; - INITIAL_NUM_WARM_SERVERS: string; - PYGAME_HIDE_SUPPORT_PROMPT: string; - INIT_CWD: string; - TEST: string; - VITEST: string; - NODE_ENV: string; - PROD: string; - DEV: string; - BASE_URL: string; - MODE: string; + MAIL: string; + KITTY_INSTALLATION_DIR: string; + npm_node_execpath: string; + LC_NUMERIC: string; + VITE_USER_NODE_ENV: string; [key: `PUBLIC_${string}`]: undefined; [key: `${string}`]: string | undefined; } diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index b76ac8fd..1f3a7e4f 100644 --- a/.svelte-kit/generated/server/internal.js +++ b/.svelte-kit/generated/server/internal.js @@ -23,7 +23,7 @@ export const options = { app: ({ head, body, assets, nonce, env }) => "\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t" + head + "\n\t\n\t\n\t\t
" + body + "
\n\t\t\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

Install NoteVault

\n\t\t\t\t\t

Get the full app experience with offline access

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t\t\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\tYou're offline. Some features may be limited.\n\t\t\t
\n\t\t
\n\t\t\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

Update Available

\n\t\t\t\t\t

A new version is ready to install

\n\t\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "1jp65kv" + version_hash: "dtzn9" }; export async function get_hooks() { diff --git a/server/database/notevault.db b/server/database/notevault.db index 061fc896..734a0472 100644 Binary files a/server/database/notevault.db and b/server/database/notevault.db differ diff --git a/src/lib/api.ts b/src/lib/api.ts index f5a6c92e..1d53b706 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -10,9 +10,12 @@ type RequestInit = { credentials?: RequestCredentials; }; -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api'; +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:12001/api'; class ApiClient { + private cache = new Map(); + private readonly CACHE_TTL = 5000; // 5 seconds cache + private getAuthHeaders(): HeadersInit { if (!browser) return {}; @@ -20,10 +23,30 @@ class ApiClient { return token ? { Authorization: `Bearer ${token}` } : {}; } + private getCacheKey(endpoint: string, options: RequestInit = {}): string { + return `${options.method || 'GET'}:${endpoint}`; + } + + private isValidCache(timestamp: number): boolean { + return Date.now() - timestamp < this.CACHE_TTL; + } + private async request( endpoint: string, - options: RequestInit = {} + options: RequestInit = {}, + retryCount = 0 ): Promise { + const cacheKey = this.getCacheKey(endpoint, options); + const method = options.method || 'GET'; + + // Only cache GET requests + if (method === 'GET') { + const cached = this.cache.get(cacheKey); + if (cached && this.isValidCache(cached.timestamp)) { + return cached.data; + } + } + const url = `${API_BASE_URL}${endpoint}`; const config: RequestInit = { @@ -39,11 +62,35 @@ class ApiClient { const response = await fetch(url, config); if (!response.ok) { + // Handle rate limiting with exponential backoff + if (response.status === 429 && retryCount < 3) { + const retryAfter = response.headers.get('Retry-After'); + const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, retryCount) * 1000; + + console.warn(`Rate limited. Retrying after ${delay}ms (attempt ${retryCount + 1}/3)`); + + await new Promise(resolve => setTimeout(resolve, delay)); + return this.request(endpoint, options, retryCount + 1); + } + const error = await response.json().catch(() => ({ error: 'Network error' })); + + // Provide user-friendly error messages for rate limiting + if (response.status === 429) { + throw new Error('Too many requests. Please wait a moment and try again.'); + } + throw new Error(error.error || `HTTP ${response.status}`); } - return await response.json(); + const data = await response.json(); + + // Cache GET requests + if (method === 'GET') { + this.cache.set(cacheKey, { data, timestamp: Date.now() }); + } + + return data; } catch (error) { console.error('API request failed:', error); throw error; @@ -149,8 +196,20 @@ class ApiClient { } // Note endpoints - async getWorkspaceNotes(workspaceId: string) { - return this.request(`/notes/workspace/${workspaceId}`); + async getWorkspaceNotes(workspaceId: string, params?: { + limit?: number; + offset?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + }) { + const query = new URLSearchParams(); + if (params?.limit) query.set('limit', params.limit.toString()); + if (params?.offset) query.set('offset', params.offset.toString()); + if (params?.sortBy) query.set('sortBy', params.sortBy); + if (params?.sortOrder) query.set('sortOrder', params.sortOrder); + + const url = `/notes/workspace/${workspaceId}${query.toString() ? '?' + query.toString() : ''}`; + return this.request(url); } async getNote(id: string) { @@ -167,6 +226,7 @@ class ApiClient { color: string; tags?: string[]; isPublic?: boolean; + collectionId?: string; }) { return this.request('/notes', { method: 'POST', diff --git a/src/lib/components/ChatMembersModal.svelte b/src/lib/components/ChatMembersModal.svelte index be96dead..4eb3abad 100644 --- a/src/lib/components/ChatMembersModal.svelte +++ b/src/lib/components/ChatMembersModal.svelte @@ -69,7 +69,7 @@ } $: filteredUsers = $onlineUsers.filter(user => - user.displayName.toLowerCase().includes(searchQuery.toLowerCase()) || + (user.displayName || user.username || '')?.toLowerCase().includes(searchQuery.toLowerCase()) || user.username?.toLowerCase().includes(searchQuery.toLowerCase()) ); @@ -111,7 +111,7 @@
{user.displayName}
@@ -120,7 +120,7 @@
- {user.displayName} + {user.displayName || user.username} {#if getRoleIcon(user.role)}
@@ -133,7 +136,7 @@ {#each adminNavigation as item} {item.name} diff --git a/src/lib/stores/chat.ts b/src/lib/stores/chat.ts index 7257af8d..29a00d7b 100644 --- a/src/lib/stores/chat.ts +++ b/src/lib/stores/chat.ts @@ -25,12 +25,15 @@ const getCurrentUserId = (): string | null => { export const chatStore = { connect: () => { - if (!browser || socket?.connected) return; + if (!browser) return; + + // If there's already a connected socket, don't create a new one + if (socket && socket.connected) return; // Use the same base URL as the API but without the /api path for WebSocket const wsUrl = import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL.replace('/api', '') - : 'http://localhost:3001'; + : 'http://localhost:12001'; socket = io(wsUrl, { transports: ['websocket', 'polling'] diff --git a/src/lib/stores/workspaces.ts b/src/lib/stores/workspaces.ts index 211fb678..9d52d1ad 100644 --- a/src/lib/stores/workspaces.ts +++ b/src/lib/stores/workspaces.ts @@ -51,9 +51,16 @@ export const workspaceStore = { } }, - loadWorkspaceNotes: async (workspaceId: string) => { + loadWorkspaceNotes: async (workspaceId: string, params?: { + limit?: number; + offset?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + append?: boolean; + }) => { try { - const notesData = await api.getWorkspaceNotes(workspaceId); + const { append = false, ...queryParams } = params || {}; + const notesData = await api.getWorkspaceNotes(workspaceId, queryParams); const formattedNotes: Note[] = notesData.map((note: any) => ({ id: note.id, title: note.title, @@ -69,10 +76,20 @@ export const workspaceStore = { updatedAt: new Date(note.updatedAt), isPublic: note.isPublic })); - workspaceNotes.set(formattedNotes); + + if (append) { + workspaceNotes.update(notes => [...notes, ...formattedNotes]); + } else { + workspaceNotes.set(formattedNotes); + } + + return formattedNotes; } catch (error) { console.error('Failed to load workspace notes:', error); - workspaceNotes.set([]); + if (!params?.append) { + workspaceNotes.set([]); + } + return []; } }, @@ -210,6 +227,7 @@ export const workspaceStore = { color: string; tags?: string[]; isPublic?: boolean; + collectionId?: string; }) => { try { const newNoteData = await api.createNote({ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 44ea3d02..be48be10 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,6 +6,8 @@ import { authStore, isAuthenticated, isLoading } from '$lib/stores/auth'; import { showCreateWorkspaceModal } from '$lib/stores/modals'; import { goto } from '$app/navigation'; + import { navigating } from '$app/stores'; + import { chatStore } from '$lib/stores/chat'; import Sidebar from '$lib/components/Sidebar.svelte'; import CommandPalette from '$lib/components/CommandPalette.svelte'; import CreateWorkspaceModal from '$lib/components/CreateWorkspaceModal.svelte'; @@ -26,8 +28,18 @@ let rightPanelVisible = false; let focusModeActive = false; + let initialized = false; + onMount(async () => { - authStore.checkAuth(); + if (!initialized) { + initialized = true; + await authStore.checkAuth(); + + // Initialize chat connection only once after auth is checked + if (browser) { + chatStore.connect(); + } + } if (browser) { // Initialize PWA functionality @@ -291,7 +303,7 @@ } $: { - if (!$isLoading && !$isAuthenticated && !publicRoutes.includes($page.url.pathname)) { + if (initialized && !$isLoading && !$isAuthenticated && !publicRoutes.includes($page.url.pathname)) { goto('/login'); } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9c5a72a4..f500967c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -24,7 +24,6 @@ onMount(async () => { loadWorkspacesWithLoading(); - chatStore.connect(); // Load real data await loadDashboardData(); diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index aa78599c..b9407289 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -17,9 +17,6 @@ const emojis = ['😀', '😂', '😍', '🤔', '👍', '👎', '❤️', '🎉', '🔥', '💯']; onMount(() => { - if (!$isConnected) { - chatStore.connect(); - } // Load public chat messages only (exclude workspace channels) chatStore.loadMessages({ channel: 'public' }); }); @@ -157,14 +154,7 @@ bind:this={messagesContainer} class="flex-1 overflow-y-auto px-4 py-4 space-y-1" > - {#if !$isConnected} -
-
-
-

Connecting to chat...

-
-
- {:else if $chatMessages.length === 0} + {#if $chatMessages.length === 0}
diff --git a/src/routes/workspaces/[id]/+page.svelte b/src/routes/workspaces/[id]/+page.svelte index 4c977cc9..20f5aad4 100644 --- a/src/routes/workspaces/[id]/+page.svelte +++ b/src/routes/workspaces/[id]/+page.svelte @@ -75,6 +75,15 @@ let editingCollection: NoteCollection | null = null; let parentCollection: NoteCollection | null = null; + // Infinite scrolling state + let isLoadingNotes = false; + let hasMoreNotes = true; + let notesOffset = 0; + const notesLimit = 50; // Load 50 notes at a time + let visibleBounds = { left: 0, right: 0, top: 0, bottom: 0 }; + let loadingTriggerDistance = 2000; // Load more when within 2000px of edge + let infiniteScrollThrottle: ReturnType | null = null; + function sanitizeHtml(html: string): string { return DOMPurify.sanitize(html.replace(/\n/g, '
')); } @@ -106,6 +115,37 @@ $: filteredNotes = $filteredAndSortedNotes($workspaceNotes); $: workspaceAvailableTags = $availableTags($workspaceNotes); + // Virtual scrolling - only render visible notes for performance + $: visibleNotes = getVisibleNotes(filteredNotes); + + function getVisibleNotes(notes: Note[]) { + if (notes.length === 0) return notes; + + // Add padding around visible area for smooth scrolling + const padding = 500; + const viewBounds = { + left: visibleBounds.left - padding, + right: visibleBounds.right + padding, + top: visibleBounds.top - padding, + bottom: visibleBounds.bottom + padding + }; + + return notes.filter(note => { + const noteLeft = note.position?.x || 0; + const noteTop = note.position?.y || 0; + const noteRight = noteLeft + (note.size?.width || 300); + const noteBottom = noteTop + (note.size?.height || 200); + + // Check if note intersects with visible bounds + return !( + noteRight < viewBounds.left || + noteLeft > viewBounds.right || + noteBottom < viewBounds.top || + noteTop > viewBounds.bottom + ); + }); + } + // Get workspace collections (with fallback for empty state) $: workspaceCollections = ($collectionsTree && Array.isArray($collectionsTree)) ? $collectionsTree.filter(c => c.workspaceId === workspaceId) @@ -134,7 +174,12 @@ // Load real workspace data, notes, and members Promise.all([ workspaceStore.loadWorkspace(workspaceId), - workspaceStore.loadWorkspaceNotes(workspaceId), + workspaceStore.loadWorkspaceNotes(workspaceId, { + limit: notesLimit, + offset: 0, + sortBy: 'updatedAt', + sortOrder: 'desc' + }), workspaceStore.loadWorkspaceMembers(workspaceId) ]).then(([workspace, notes, members]) => { console.log('Workspace loaded:', workspace); @@ -142,6 +187,16 @@ console.log('Members loaded:', members.length, 'members'); console.log('Collections loaded:', $collectionsTree.length, 'collections'); workspaceMembers = members; + + // Set initial pagination state + notesOffset = notes.length; + hasMoreNotes = notes.length >= notesLimit; + + // Initialize bounds and check for infinite scroll + setTimeout(() => { + updateVisibleBounds(); + checkInfiniteScroll(); + }, 100); }).catch(error => { console.error('Failed to load workspace data:', error); // Set error state or show error message to user @@ -168,14 +223,23 @@ } // Cleanup resize listener window.removeEventListener('resize', updateDeviceType); + // Cleanup infinite scroll throttle + if (infiniteScrollThrottle) { + clearTimeout(infiniteScrollThrottle); + } }; }); function handleCanvasMouseDown(event: MouseEvent) { - if (event.target === canvasElement) { - isPanning = true; - lastPanPoint = { x: event.clientX, y: event.clientY }; - canvasElement.style.cursor = 'grabbing'; + // Only start panning if clicking on the canvas itself, not on a note + const target = event.target as Element; + if (target === canvasElement || target.closest('#workspace-canvas') === canvasElement) { + // Check if the click is on a note card - if so, don't pan + if (!target.closest('[draggable="true"]')) { + isPanning = true; + lastPanPoint = { x: event.clientX, y: event.clientY }; + canvasElement.style.cursor = 'grabbing'; + } } } @@ -194,9 +258,14 @@ // Update cursor position for collaboration if (canvasElement && $currentUser) { const rect = canvasElement.getBoundingClientRect(); - const x = (event.clientX - rect.left - canvasOffset.x) / canvasScale; - const y = (event.clientY - rect.top - canvasOffset.y) / canvasScale; - updateCursor(x, y, `workspace-${workspaceId}`); + const screenX = event.clientX - rect.left; + const screenY = event.clientY - rect.top; + + // Transform to canvas coordinates + const canvasX = (screenX - canvasOffset.x) / canvasScale; + const canvasY = (screenY - canvasOffset.y) / canvasScale; + + updateCursor(canvasX, canvasY, `workspace-${workspaceId}`); } } @@ -215,6 +284,105 @@ function updateCanvasTransform() { if (canvasElement) { canvasElement.style.transform = `translate(${canvasOffset.x}px, ${canvasOffset.y}px) scale(${canvasScale})`; + updateVisibleBounds(); + throttledCheckInfiniteScroll(); + } + } + + function throttledCheckInfiniteScroll() { + if (infiniteScrollThrottle) { + clearTimeout(infiniteScrollThrottle); + } + infiniteScrollThrottle = setTimeout(() => { + checkInfiniteScroll(); + infiniteScrollThrottle = null; + }, 100); // Throttle to check at most every 100ms + } + + function updateVisibleBounds() { + if (!canvasElement) return; + + const rect = canvasElement.getBoundingClientRect(); + const inverseScale = 1 / canvasScale; + + visibleBounds = { + left: (-canvasOffset.x) * inverseScale, + right: (-canvasOffset.x + rect.width) * inverseScale, + top: (-canvasOffset.y) * inverseScale, + bottom: (-canvasOffset.y + rect.height) * inverseScale + }; + } + + async function checkInfiniteScroll() { + if (isLoadingNotes || !hasMoreNotes) return; + + // Since the canvas is now infinite, we load more notes based on + // how much of the visible area contains notes vs empty space + const notesInView = visibleNotes.length; + const totalNotesLoaded = $workspaceNotes.length; + + // If we have very few visible notes compared to total notes, + // or if we're viewing an area with sparse note density, load more + const viewAreaSize = (visibleBounds.right - visibleBounds.left) * + (visibleBounds.bottom - visibleBounds.top); + const notesDensity = notesInView / (viewAreaSize / 100000); // notes per 100k px² + + // Load more if density is low and we have more notes available + if (notesDensity < 0.1 && totalNotesLoaded % notesLimit === 0) { + await loadMoreNotes(); + } + } + + function getNotesBounds() { + if ($workspaceNotes.length === 0) { + return { left: 0, right: 1000, top: 0, bottom: 1000 }; + } + + let minX = Infinity, maxX = -Infinity; + let minY = Infinity, maxY = -Infinity; + + for (const note of $workspaceNotes) { + const left = note.position?.x || 0; + const top = note.position?.y || 0; + const right = left + (note.size?.width || 300); + const bottom = top + (note.size?.height || 200); + + minX = Math.min(minX, left); + maxX = Math.max(maxX, right); + minY = Math.min(minY, top); + maxY = Math.max(maxY, bottom); + } + + return { + left: minX, + right: maxX, + top: minY, + bottom: maxY + }; + } + + async function loadMoreNotes() { + if (isLoadingNotes || !hasMoreNotes) return; + + isLoadingNotes = true; + try { + const newNotes = await workspaceStore.loadWorkspaceNotes(workspaceId, { + limit: notesLimit, + offset: notesOffset, + sortBy: 'updatedAt', + sortOrder: 'desc', + append: true + }); + + if (newNotes.length < notesLimit) { + hasMoreNotes = false; + } else { + notesOffset += newNotes.length; + } + } catch (error) { + console.error('Failed to load more notes:', error); + } finally { + isLoadingNotes = false; } } @@ -276,10 +444,18 @@ function handleNoteDragStart(event: CustomEvent<{ note: Note }>) { draggedNote = event.detail.note; + // Change cursor to indicate dragging + if (canvasElement) { + canvasElement.style.cursor = 'grabbing'; + } } function handleNoteDragEnd(event: CustomEvent<{ note: Note }>) { draggedNote = null; + // Reset cursor + if (canvasElement) { + canvasElement.style.cursor = 'grab'; + } } function handleNoteClick(event: CustomEvent<{ note: Note }>) { @@ -329,29 +505,65 @@ event.preventDefault(); if (draggedNote) { const rect = canvasElement.getBoundingClientRect(); - const x = (event.clientX - rect.left - canvasOffset.x) / canvasScale; - const y = (event.clientY - rect.top - canvasOffset.y) / canvasScale; + + // Convert screen coordinates to canvas coordinates + // Account for both canvas offset and scale + const screenX = event.clientX - rect.left; + const screenY = event.clientY - rect.top; + + // Transform to canvas coordinates + const canvasX = (screenX - canvasOffset.x) / canvasScale; + const canvasY = (screenY - canvasOffset.y) / canvasScale; workspaceStore.updateNote(draggedNote.id, { - position: { x, y } + position: { + x: Math.round(canvasX), + y: Math.round(canvasY) + } }); } } function handleCanvasDragOver(event: DragEvent) { event.preventDefault(); + + // Visual feedback during drag + if (draggedNote && canvasElement) { + const rect = canvasElement.getBoundingClientRect(); + const screenX = event.clientX - rect.left; + const screenY = event.clientY - rect.top; + + // Transform to canvas coordinates (same logic as drop) + const canvasX = (screenX - canvasOffset.x) / canvasScale; + const canvasY = (screenY - canvasOffset.y) / canvasScale; + + // Update drag preview position (stored for reference) + draggedNote.dragPreviewPosition = { x: canvasX, y: canvasY }; + } } async function handleCreateNote() { if (!newNoteTitle.trim()) return; + // Calculate position at the center of the current viewport + const viewCenterX = (visibleBounds.left + visibleBounds.right) / 2; + const viewCenterY = (visibleBounds.top + visibleBounds.bottom) / 2; + + // Add some randomness to avoid overlapping if multiple notes are created + const offsetX = (Math.random() - 0.5) * 200; // ±100px random offset + const offsetY = (Math.random() - 0.5) * 200; + await workspaceStore.createNote(workspaceId, { title: newNoteTitle, content: newNoteContent, type: newNoteType, color: newNoteColor, tags: newNoteTags, - collectionId: newNoteCollectionId + collectionId: newNoteCollectionId, + position: { + x: Math.round(viewCenterX + offsetX), + y: Math.round(viewCenterY + offsetY) + } }); showCreateModal = false; @@ -370,7 +582,7 @@ } function handleCanvasKeyDown(event: KeyboardEvent) { - const step = 50; + const step = event.shiftKey ? 200 : 50; // Larger steps with Shift const zoomStep = 0.1; switch (event.key) { @@ -409,6 +621,12 @@ event.preventDefault(); resetCanvas(); break; + case 'h': // Go to origin (home) + event.preventDefault(); + canvasOffset = { x: 0, y: 0 }; + canvasScale = 1; + updateCanvasTransform(); + break; case 'Enter': case ' ': event.preventDefault(); @@ -504,12 +722,9 @@ } } - // Initialize chat when workspace loads + // Load workspace-specific messages when workspace loads $: if (workspaceId) { - if (!$isConnected) { - chatStore.connect(); - } - // Load workspace-specific messages + // Load workspace-specific messages (connection is handled in layout) chatStore.loadMessages({ channel: `workspace-${workspaceId}` }); chatStore.loadOnlineUsers(); chatStore.joinWorkspace(workspaceId); @@ -876,7 +1091,7 @@ bind:this={canvasElement} id="workspace-canvas" class="absolute inset-0 cursor-grab touch-none select-none" - style="transform-origin: 0 0;" + style="transform-origin: 0 0; user-select: none;" on:mousedown={handleCanvasMouseDown} on:mousemove={handleCanvasMouseMove} on:mouseup={handleCanvasMouseUp} @@ -892,20 +1107,27 @@ aria-label="Interactive workspace canvas for managing notes" on:keydown={handleCanvasKeyDown} > - -
- - - - - - - - -
+ +
- - {#each filteredNotes as note (note.id)} + + {#each visibleNotes as note (note.id)} {/each} + + +
+
+
+ X: + {Math.round(-canvasOffset.x / canvasScale)} +
+
+ Y: + {Math.round(-canvasOffset.y / canvasScale)} +
+
+ Zoom: + {Math.round(canvasScale * 100)}% +
+
+
+ Keys: + Arrows=Move • H=Origin • +/-=Zoom • 0=Reset • Space=New Note +
+
+ + + {#if import.meta.env.DEV} +
+
Total Notes: {filteredNotes.length}
+
Visible: {visibleNotes.length}
+
Loaded: {$workspaceNotes.length}
+
Has More: {hasMoreNotes ? 'Yes' : 'No'}
+
Loading: {isLoadingNotes ? 'Yes' : 'No'}
+ {#if draggedNote} +
Dragging: {draggedNote.title}
+ {#if draggedNote.dragPreviewPosition} +
Drop at: {Math.round(draggedNote.dragPreviewPosition.x)}, {Math.round(draggedNote.dragPreviewPosition.y)}
+ {/if} + {/if} +
View Bounds:
+
L: {Math.round(visibleBounds.left)} R: {Math.round(visibleBounds.right)}
+
T: {Math.round(visibleBounds.top)} B: {Math.round(visibleBounds.bottom)}
+
Canvas:
+
Offset: {Math.round(canvasOffset.x)}, {Math.round(canvasOffset.y)}
+
Scale: {Math.round(canvasScale * 100)}%
+
+ {/if} {#if $collaborationStatus.connected} @@ -923,6 +1190,14 @@ showSelections={false} /> {/if} + + + {#if isLoadingNotes} +
+
+ Loading more notes... +
+ {/if}
@@ -949,6 +1224,19 @@
+ + {#if hasMoreNotes && !isLoadingNotes} +
+ +
+ {/if} + {#if isMobile}