diff --git a/surfsense_backend/app/routes/airtable_add_connector_route.py b/surfsense_backend/app/routes/airtable_add_connector_route.py index fa124f1c2..61f33be8c 100644 --- a/surfsense_backend/app/routes/airtable_add_connector_route.py +++ b/surfsense_backend/app/routes/airtable_add_connector_route.py @@ -24,6 +24,10 @@ from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase from app.users import current_active_user +# Re-export for backward compatibility - this import is used by other modules +# that import refresh_airtable_token from this module +from app.utils.connector_auth import refresh_airtable_token as refresh_airtable_token # noqa: F401 + logger = logging.getLogger(__name__) router = APIRouter() @@ -286,78 +290,3 @@ async def airtable_callback( raise HTTPException( status_code=500, detail=f"Failed to complete Airtable OAuth: {e!s}" ) from e - - -async def refresh_airtable_token( - session: AsyncSession, connector: SearchSourceConnector -): - """ - Refresh the Airtable access token for a connector. - - Args: - session: Database session - connector: Airtable connector to refresh - - Returns: - Updated connector object - """ - try: - logger.info(f"Refreshing Airtable token for connector {connector.id}") - - credentials = AirtableAuthCredentialsBase.from_dict(connector.config) - auth_header = make_basic_auth_header( - config.AIRTABLE_CLIENT_ID, config.AIRTABLE_CLIENT_SECRET - ) - - # Prepare token refresh data - refresh_data = { - "grant_type": "refresh_token", - "refresh_token": credentials.refresh_token, - "client_id": config.AIRTABLE_CLIENT_ID, - "client_secret": config.AIRTABLE_CLIENT_SECRET, - } - - async with httpx.AsyncClient() as client: - token_response = await client.post( - TOKEN_URL, - data=refresh_data, - headers={ - "Content-Type": "application/x-www-form-urlencoded", - "Authorization": auth_header, - }, - timeout=30.0, - ) - - if token_response.status_code != 200: - raise HTTPException( - status_code=400, detail="Token refresh failed: {token_response.text}" - ) - - token_json = token_response.json() - - # Calculate expiration time (UTC, tz-aware) - expires_at = None - if token_json.get("expires_in"): - now_utc = datetime.now(UTC) - expires_at = now_utc + timedelta(seconds=int(token_json["expires_in"])) - - # Update credentials object - credentials.access_token = token_json["access_token"] - credentials.expires_in = token_json.get("expires_in") - credentials.expires_at = expires_at - credentials.scope = token_json.get("scope") - - # Update connector config - connector.config = credentials.to_dict() - await session.commit() - await session.refresh(connector) - - logger.info( - f"Successfully refreshed Airtable token for connector {connector.id}" - ) - - return connector - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to refresh Airtable token: {e!s}" - ) from e diff --git a/surfsense_backend/app/routes/chats_routes.py b/surfsense_backend/app/routes/chats_routes.py index d7aff102b..21187e296 100644 --- a/surfsense_backend/app/routes/chats_routes.py +++ b/surfsense_backend/app/routes/chats_routes.py @@ -1,7 +1,9 @@ +import logging + from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from langchain.schema import AIMessage, HumanMessage -from sqlalchemy.exc import IntegrityError, OperationalError +from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy.orm import selectinload @@ -34,6 +36,8 @@ validate_top_k, ) +logger = logging.getLogger(__name__) + router = APIRouter() @@ -180,19 +184,29 @@ async def create_chat( return db_chat except HTTPException: raise - except IntegrityError: + except IntegrityError as e: await session.rollback() + logger.warning("Chat creation failed due to integrity error: %s", e) raise HTTPException( status_code=400, detail="Database constraint violation. Please check your input data.", ) from None - except OperationalError: + except OperationalError as e: await session.rollback() + logger.error("Database operational error during chat creation: %s", e) raise HTTPException( status_code=503, detail="Database operation failed. Please try again later." ) from None - except Exception: + except SQLAlchemyError as e: + await session.rollback() + logger.error("Database error during chat creation: %s", e, exc_info=True) + raise HTTPException( + status_code=500, + detail="An unexpected error occurred while creating the chat.", + ) from None + except Exception as e: await session.rollback() + logger.error("Unexpected error during chat creation: %s", e, exc_info=True) raise HTTPException( status_code=500, detail="An unexpected error occurred while creating the chat.", @@ -266,11 +280,18 @@ async def read_chats( return result.all() except HTTPException: raise - except OperationalError: + except OperationalError as e: + logger.error("Database operational error while fetching chats: %s", e) raise HTTPException( status_code=503, detail="Database operation failed. Please try again later." ) from None - except Exception: + except SQLAlchemyError as e: + logger.error("Database error while fetching chats: %s", e, exc_info=True) + raise HTTPException( + status_code=500, detail="An unexpected error occurred while fetching chats." + ) from None + except Exception as e: + logger.error("Unexpected error while fetching chats: %s", e, exc_info=True) raise HTTPException( status_code=500, detail="An unexpected error occurred while fetching chats." ) from None @@ -308,11 +329,19 @@ async def read_chat( return chat except HTTPException: raise - except OperationalError: + except OperationalError as e: + logger.error("Database operational error while fetching chat %d: %s", chat_id, e) raise HTTPException( status_code=503, detail="Database operation failed. Please try again later." ) from None - except Exception: + except SQLAlchemyError as e: + logger.error("Database error while fetching chat %d: %s", chat_id, e, exc_info=True) + raise HTTPException( + status_code=500, + detail="An unexpected error occurred while fetching the chat.", + ) from None + except Exception as e: + logger.error("Unexpected error while fetching chat %d: %s", chat_id, e, exc_info=True) raise HTTPException( status_code=500, detail="An unexpected error occurred while fetching the chat.", @@ -357,19 +386,29 @@ async def update_chat( return db_chat except HTTPException: raise - except IntegrityError: + except IntegrityError as e: await session.rollback() + logger.warning("Chat update failed due to integrity error for chat %d: %s", chat_id, e) raise HTTPException( status_code=400, detail="Database constraint violation. Please check your input data.", ) from None - except OperationalError: + except OperationalError as e: await session.rollback() + logger.error("Database operational error while updating chat %d: %s", chat_id, e) raise HTTPException( status_code=503, detail="Database operation failed. Please try again later." ) from None - except Exception: + except SQLAlchemyError as e: + await session.rollback() + logger.error("Database error while updating chat %d: %s", chat_id, e, exc_info=True) + raise HTTPException( + status_code=500, + detail="An unexpected error occurred while updating the chat.", + ) from None + except Exception as e: await session.rollback() + logger.error("Unexpected error while updating chat %d: %s", chat_id, e, exc_info=True) raise HTTPException( status_code=500, detail="An unexpected error occurred while updating the chat.", @@ -407,18 +446,28 @@ async def delete_chat( return {"message": "Chat deleted successfully"} except HTTPException: raise - except IntegrityError: + except IntegrityError as e: await session.rollback() + logger.warning("Chat deletion failed due to integrity error for chat %d: %s", chat_id, e) raise HTTPException( status_code=400, detail="Cannot delete chat due to existing dependencies." ) from None - except OperationalError: + except OperationalError as e: await session.rollback() + logger.error("Database operational error while deleting chat %d: %s", chat_id, e) raise HTTPException( status_code=503, detail="Database operation failed. Please try again later." ) from None - except Exception: + except SQLAlchemyError as e: + await session.rollback() + logger.error("Database error while deleting chat %d: %s", chat_id, e, exc_info=True) + raise HTTPException( + status_code=500, + detail="An unexpected error occurred while deleting the chat.", + ) from None + except Exception as e: await session.rollback() + logger.error("Unexpected error while deleting chat %d: %s", chat_id, e, exc_info=True) raise HTTPException( status_code=500, detail="An unexpected error occurred while deleting the chat.", diff --git a/surfsense_backend/app/routes/podcasts_routes.py b/surfsense_backend/app/routes/podcasts_routes.py index deb9d9744..92acf9932 100644 --- a/surfsense_backend/app/routes/podcasts_routes.py +++ b/surfsense_backend/app/routes/podcasts_routes.py @@ -1,3 +1,4 @@ +import logging import os from pathlib import Path @@ -26,6 +27,8 @@ from app.users import current_active_user from app.utils.rbac import check_permission +logger = logging.getLogger(__name__) + router = APIRouter() @@ -54,21 +57,24 @@ async def create_podcast( return db_podcast except HTTPException as he: raise he - except IntegrityError: + except IntegrityError as e: await session.rollback() + logger.warning("Podcast creation failed due to integrity error: %s", e) raise HTTPException( status_code=400, detail="Podcast creation failed due to constraint violation", ) from None - except SQLAlchemyError: + except SQLAlchemyError as e: await session.rollback() + logger.error("Database error while creating podcast: %s", e, exc_info=True) raise HTTPException( status_code=500, detail="Database error occurred while creating podcast" ) from None - except Exception: + except Exception as e: await session.rollback() + logger.error("Unexpected error while creating podcast: %s", e, exc_info=True) raise HTTPException( - status_code=500, detail="An unexpected error occurred" + status_code=500, detail="An unexpected error occurred while creating podcast" ) from None @@ -115,10 +121,16 @@ async def read_podcasts( return result.scalars().all() except HTTPException: raise - except SQLAlchemyError: + except SQLAlchemyError as e: + logger.error("Database error while fetching podcasts: %s", e, exc_info=True) raise HTTPException( status_code=500, detail="Database error occurred while fetching podcasts" ) from None + except Exception as e: + logger.error("Unexpected error while fetching podcasts: %s", e, exc_info=True) + raise HTTPException( + status_code=500, detail="An unexpected error occurred while fetching podcasts" + ) from None @router.get("/podcasts/{podcast_id}", response_model=PodcastRead) @@ -153,10 +165,16 @@ async def read_podcast( return podcast except HTTPException as he: raise he - except SQLAlchemyError: + except SQLAlchemyError as e: + logger.error("Database error while fetching podcast: %s", e, exc_info=True) raise HTTPException( status_code=500, detail="Database error occurred while fetching podcast" ) from None + except Exception as e: + logger.error("Unexpected error while fetching podcast: %s", e, exc_info=True) + raise HTTPException( + status_code=500, detail="An unexpected error occurred while fetching podcast" + ) from None @router.put("/podcasts/{podcast_id}", response_model=PodcastRead) @@ -199,11 +217,18 @@ async def update_podcast( raise HTTPException( status_code=400, detail="Update failed due to constraint violation" ) from None - except SQLAlchemyError: + except SQLAlchemyError as e: await session.rollback() + logger.error("Database error while updating podcast: %s", e, exc_info=True) raise HTTPException( status_code=500, detail="Database error occurred while updating podcast" ) from None + except Exception as e: + await session.rollback() + logger.error("Unexpected error while updating podcast: %s", e, exc_info=True) + raise HTTPException( + status_code=500, detail="An unexpected error occurred while updating podcast" + ) from None @router.delete("/podcasts/{podcast_id}", response_model=dict) @@ -237,11 +262,18 @@ async def delete_podcast( return {"message": "Podcast deleted successfully"} except HTTPException as he: raise he - except SQLAlchemyError: + except SQLAlchemyError as e: await session.rollback() + logger.error("Database error while deleting podcast: %s", e, exc_info=True) raise HTTPException( status_code=500, detail="Database error occurred while deleting podcast" ) from None + except Exception as e: + await session.rollback() + logger.error("Unexpected error while deleting podcast: %s", e, exc_info=True) + raise HTTPException( + status_code=500, detail="An unexpected error occurred while deleting podcast" + ) from None async def generate_chat_podcast_with_new_session( @@ -260,9 +292,7 @@ async def generate_chat_podcast_with_new_session( session, chat_id, search_space_id, user_id, podcast_title, user_prompt ) except Exception as e: - import logging - - logging.error(f"Error generating podcast from chat: {e!s}") + logger.error("Error generating podcast from chat: %s", e, exc_info=True) @router.post("/podcasts/generate") diff --git a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py index cf6824db8..9839be77e 100644 --- a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py @@ -8,8 +8,8 @@ from app.config import config from app.connectors.airtable_connector import AirtableConnector from app.db import Document, DocumentType, SearchSourceConnectorType -from app.routes.airtable_add_connector_route import refresh_airtable_token from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase +from app.utils.connector_auth import refresh_airtable_token from app.services.llm_service import get_user_long_context_llm from app.services.task_logging_service import TaskLoggingService from app.utils.document_converters import ( diff --git a/surfsense_backend/app/tasks/document_processors/file_processors.py b/surfsense_backend/app/tasks/document_processors/file_processors.py index 4ae04e050..c7204a0bc 100644 --- a/surfsense_backend/app/tasks/document_processors/file_processors.py +++ b/surfsense_backend/app/tasks/document_processors/file_processors.py @@ -30,6 +30,8 @@ ) from .markdown_processor import add_received_markdown_file_document +logger = logging.getLogger(__name__) + async def add_received_file_document_using_unstructured( session: AsyncSession, @@ -473,9 +475,8 @@ async def process_file_in_background( try: os.unlink(file_path) - except Exception as e: - print("Error deleting temp file", e) - pass + except OSError as e: + logger.debug("Could not delete temp file %s: %s", file_path, e) await task_logger.log_task_progress( log_entry, @@ -598,9 +599,8 @@ async def process_file_in_background( # Clean up the temp file try: os.unlink(file_path) - except Exception as e: - print("Error deleting temp file", e) - pass + except OSError as e: + logger.debug("Could not delete temp file %s: %s", file_path, e) # Process transcription as markdown document result = await add_received_markdown_file_document( @@ -743,9 +743,8 @@ async def process_file_in_background( try: os.unlink(file_path) - except Exception as e: - print("Error deleting temp file", e) - pass + except OSError as e: + logger.debug("Could not delete temp file %s: %s", file_path, e) # Pass the documents to the existing background task result = await add_received_file_document_using_unstructured( @@ -812,9 +811,8 @@ async def process_file_in_background( try: os.unlink(file_path) - except Exception as e: - print("Error deleting temp file", e) - pass + except OSError as e: + logger.debug("Could not delete temp file %s: %s", file_path, e) # Get markdown documents from the result markdown_documents = await result.aget_markdown_documents( @@ -971,9 +969,8 @@ async def process_file_in_background( try: os.unlink(file_path) - except Exception as e: - print("Error deleting temp file", e) - pass + except OSError as e: + logger.debug("Could not delete temp file %s: %s", file_path, e) await task_logger.log_task_progress( log_entry, diff --git a/surfsense_backend/app/utils/connector_auth.py b/surfsense_backend/app/utils/connector_auth.py new file mode 100644 index 000000000..ec517d00c --- /dev/null +++ b/surfsense_backend/app/utils/connector_auth.py @@ -0,0 +1,105 @@ +""" +Utility functions for connector authentication. + +This module provides authentication helper functions for various connectors +to avoid circular imports between routes and connector indexers. +""" + +import base64 +import logging +from datetime import UTC, datetime, timedelta + +import httpx +from fastapi import HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import config +from app.db import SearchSourceConnector +from app.schemas.airtable_auth_credentials import AirtableAuthCredentialsBase + +logger = logging.getLogger(__name__) + +# Airtable OAuth endpoints +AIRTABLE_TOKEN_URL = "https://airtable.com/oauth2/v1/token" + + +def make_basic_auth_header(client_id: str, client_secret: str) -> str: + """Create a Basic authentication header.""" + credentials = f"{client_id}:{client_secret}".encode() + b64 = base64.b64encode(credentials).decode("ascii") + return f"Basic {b64}" + + +async def refresh_airtable_token( + session: AsyncSession, connector: SearchSourceConnector +): + """ + Refresh the Airtable access token for a connector. + + Args: + session: Database session + connector: Airtable connector to refresh + + Returns: + Updated connector object + """ + try: + logger.info(f"Refreshing Airtable token for connector {connector.id}") + + credentials = AirtableAuthCredentialsBase.from_dict(connector.config) + auth_header = make_basic_auth_header( + config.AIRTABLE_CLIENT_ID, config.AIRTABLE_CLIENT_SECRET + ) + + # Prepare token refresh data + refresh_data = { + "grant_type": "refresh_token", + "refresh_token": credentials.refresh_token, + "client_id": config.AIRTABLE_CLIENT_ID, + "client_secret": config.AIRTABLE_CLIENT_SECRET, + } + + async with httpx.AsyncClient() as client: + token_response = await client.post( + AIRTABLE_TOKEN_URL, + data=refresh_data, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": auth_header, + }, + timeout=30.0, + ) + + if token_response.status_code != 200: + raise HTTPException( + status_code=400, detail="Token refresh failed: {token_response.text}" + ) + + token_json = token_response.json() + + # Calculate expiration time (UTC, tz-aware) + expires_at = None + if token_json.get("expires_in"): + now_utc = datetime.now(UTC) + expires_at = now_utc + timedelta(seconds=int(token_json["expires_in"])) + + # Update credentials object + credentials.access_token = token_json["access_token"] + credentials.expires_in = token_json.get("expires_in") + credentials.expires_at = expires_at + credentials.scope = token_json.get("scope") + + # Update connector config + connector.config = credentials.to_dict() + await session.commit() + await session.refresh(connector) + + logger.info( + f"Successfully refreshed Airtable token for connector {connector.id}" + ) + + return connector + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to refresh Airtable token: {e!s}" + ) from e diff --git a/surfsense_browser_extension/background/index.ts b/surfsense_browser_extension/background/index.ts index 8d66cf117..affc77a87 100644 --- a/surfsense_browser_extension/background/index.ts +++ b/surfsense_browser_extension/background/index.ts @@ -1,4 +1,5 @@ import { Storage } from "@plasmohq/storage"; +import { logger } from "~/lib/logger"; import { getRenderedHtml, initQueues, initWebHistory } from "~utils/commons"; import type { WebHistory } from "~utils/interfaces"; @@ -7,7 +8,7 @@ chrome.tabs.onCreated.addListener(async (tab: any) => { await initWebHistory(tab.id); await initQueues(tab.id); } catch (error) { - console.log(error); + logger.error("Error in onCreated listener", error); } }); diff --git a/surfsense_browser_extension/background/messages/savedata.ts b/surfsense_browser_extension/background/messages/savedata.ts index 8719eb365..8ed98f706 100644 --- a/surfsense_browser_extension/background/messages/savedata.ts +++ b/surfsense_browser_extension/background/messages/savedata.ts @@ -1,6 +1,7 @@ import type { PlasmoMessaging } from "@plasmohq/messaging"; import { Storage } from "@plasmohq/storage"; +import { logger } from "~/lib/logger"; import { emptyArr, webhistoryToLangChainDocument } from "~utils/commons"; const clearMemory = async () => { @@ -18,7 +19,6 @@ const clearMemory = async () => { //Main Cleanup COde chrome.tabs.query({}, async (tabs) => { //Get Active Tabs Ids - // console.log("Event Tabs",tabs) let actives = tabs.map((tab) => { if (tab.id) { return tab.id; @@ -60,7 +60,7 @@ const clearMemory = async () => { }); }); } catch (error) { - console.log(error); + logger.error("Error clearing memory", error); } }; @@ -90,7 +90,7 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { // Log first item to debug metadata structure if (toSaveFinally.length > 0) { - console.log("First item metadata:", toSaveFinally[0].metadata); + logger.debug("First item metadata:", toSaveFinally[0].metadata); } // Create content array for documents in the format expected by the new API @@ -119,7 +119,7 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { search_space_id: search_space_id, }; - console.log("toSend", toSend); + logger.debug("toSend", toSend); const requestOptions = { method: "POST", @@ -143,7 +143,7 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { } } } catch (error) { - console.log(error); + logger.error("Error in savedata handler", error); } }; diff --git a/surfsense_browser_extension/background/messages/savesnapshot.ts b/surfsense_browser_extension/background/messages/savesnapshot.ts index 8ab60b9fa..1ee6fb4bf 100644 --- a/surfsense_browser_extension/background/messages/savesnapshot.ts +++ b/surfsense_browser_extension/background/messages/savesnapshot.ts @@ -3,6 +3,7 @@ import type { PlasmoMessaging } from "@plasmohq/messaging"; import { Storage } from "@plasmohq/storage"; import { convertHtmlToMarkdown } from "dom-to-semantic-markdown"; import { DOMParser } from "linkedom"; +import { logger } from "~/lib/logger"; import { getRenderedHtml, webhistoryToLangChainDocument } from "~utils/commons"; import type { WebHistory } from "~utils/interfaces"; @@ -26,7 +27,7 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { const tab = tabs[0]; if (tab.id) { const tabId: number = tab.id; - console.log("tabs", tabs); + logger.debug("tabs", tabs); const result = await chrome.scripting.executeScript({ // @ts-ignore target: { tabId: tab.id }, @@ -35,7 +36,7 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { // world: "MAIN" }); - console.log("SnapRes", result); + logger.debug("SnapRes", result); const toPushInTabHistory: any = result[0].result; // const { renderedHtml, title, url, entryTime } = result[0].result; @@ -51,7 +52,7 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { delete toPushInTabHistory.renderedHtml; - console.log("toPushInTabHistory", toPushInTabHistory); + logger.debug("toPushInTabHistory", toPushInTabHistory); const urlQueueListObj: any = await storage.get("urlQueueList"); const timeQueueListObj: any = await storage.get("timeQueueList"); @@ -79,11 +80,11 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { const markdownFormat = webhistoryToLangChainDocument(tab.id, [toPushInTabHistory]); toSaveFinally.push(...markdownFormat); - console.log("toSaveFinally", toSaveFinally); + logger.debug("toSaveFinally", toSaveFinally); // Log first item to debug metadata structure if (toSaveFinally.length > 0) { - console.log("First item metadata:", toSaveFinally[0].metadata); + logger.debug("First item metadata:", toSaveFinally[0].metadata); } // Create content array for documents in the format expected by the new API @@ -135,7 +136,7 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => { } }); } catch (error) { - console.log(error); + logger.error("Error in savesnapshot handler", error); } }; diff --git a/surfsense_browser_extension/lib/logger.ts b/surfsense_browser_extension/lib/logger.ts new file mode 100644 index 000000000..d9972d54a --- /dev/null +++ b/surfsense_browser_extension/lib/logger.ts @@ -0,0 +1,72 @@ +/** + * Environment-aware logger utility for SurfSense browser extension. + * + * In development mode: Uses console.log for debugging + * In production mode: Suppresses debug logs, only shows warnings and errors + * + * Usage: + * import { logger } from '~/lib/logger'; + * logger.debug('Debug message', data); + * logger.info('Info message'); + * logger.warn('Warning message'); + * logger.error('Error message', error); + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +interface LoggerConfig { + isDevelopment: boolean; + prefix: string; +} + +const config: LoggerConfig = { + // Plasmo sets NODE_ENV during build + isDevelopment: process.env.NODE_ENV !== 'production', + prefix: '[SurfSense]', +}; + +const formatMessage = (level: LogLevel, message: string): string => { + const timestamp = new Date().toISOString(); + return `${config.prefix} [${timestamp}] [${level.toUpperCase()}] ${message}`; +}; + +export const logger = { + /** + * Debug level logging - only shows in development + */ + debug: (message: string, ...args: unknown[]): void => { + if (config.isDevelopment) { + console.log(formatMessage('debug', message), ...args); + } + }, + + /** + * Info level logging - only shows in development + */ + info: (message: string, ...args: unknown[]): void => { + if (config.isDevelopment) { + console.info(formatMessage('info', message), ...args); + } + }, + + /** + * Warning level logging - always shows + */ + warn: (message: string, ...args: unknown[]): void => { + console.warn(formatMessage('warn', message), ...args); + }, + + /** + * Error level logging - always shows + */ + error: (message: string, ...args: unknown[]): void => { + console.error(formatMessage('error', message), ...args); + }, + + /** + * Check if we're in development mode + */ + isDev: (): boolean => config.isDevelopment, +}; + +export default logger; diff --git a/surfsense_browser_extension/routes/pages/HomePage.tsx b/surfsense_browser_extension/routes/pages/HomePage.tsx index 362c64056..a1f8eed29 100644 --- a/surfsense_browser_extension/routes/pages/HomePage.tsx +++ b/surfsense_browser_extension/routes/pages/HomePage.tsx @@ -14,6 +14,7 @@ import { convertHtmlToMarkdown } from "dom-to-semantic-markdown"; import { Check, ChevronsUpDown } from "lucide-react"; import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { logger } from "~/lib/logger"; import { cn } from "~/lib/utils"; import { Button } from "~/routes/ui/button"; import { @@ -59,7 +60,7 @@ const HomePage = () => { throw new Error("Token verification failed"); } else { const res = await response.json(); - console.log(res); + logger.debug("Search spaces response", res); setSearchSpaces(res); } } catch (error) { @@ -79,7 +80,7 @@ const HomePage = () => { chrome.storage.onChanged.addListener((changes: any, areaName: string) => { if (changes.webhistory) { const webhistory = JSON.parse(changes.webhistory.newValue); - console.log("webhistory", webhistory); + logger.debug("webhistory changed", webhistory); let sum = 0; webhistory.webhistory.forEach((element: any) => { @@ -114,7 +115,7 @@ const HomePage = () => { setNoOfWebPages(0); } } catch (error) { - console.log(error); + logger.error("Error in onLoad", error); } } @@ -180,7 +181,7 @@ const HomePage = () => { }); }); } catch (error) { - console.log(error); + logger.error("Error clearing memory", error); } } diff --git a/surfsense_browser_extension/utils/commons.ts b/surfsense_browser_extension/utils/commons.ts index a750d8c44..7609d43f0 100644 --- a/surfsense_browser_extension/utils/commons.ts +++ b/surfsense_browser_extension/utils/commons.ts @@ -1,4 +1,5 @@ import { Storage } from "@plasmohq/storage"; +import { logger } from "~/lib/logger"; import type { WebHistory } from "./interfaces"; export const emptyArr: any[] = []; @@ -84,7 +85,7 @@ export const initWebHistory = async (tabId: number) => { await storage.set("webhistory", { webhistory: webHistory }); return; } catch (error) { - console.log(error); + logger.error("Error setting webhistory", error); } } else { return; diff --git a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx index 52d79d72b..4e05b0a90 100644 --- a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx +++ b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx @@ -1,5 +1,6 @@ "use client"; import { IconBrandGoogleFilled } from "@tabler/icons-react"; +import { logger } from "@/lib/logger"; import { motion } from "motion/react"; import { useTranslations } from "next-intl"; import { Logo } from "@/components/Logo"; @@ -21,11 +22,11 @@ export function GoogleLoginButton() { if (data.authorization_url) { window.location.href = data.authorization_url; } else { - console.error("No authorization URL received"); + logger.error("No authorization URL received"); } }) .catch((error) => { - console.error("Error during Google login:", error); + logger.error("Error during Google login:", error); }); }; return ( diff --git a/surfsense_web/app/api/contact/route.ts b/surfsense_web/app/api/contact/route.ts index 0af47dfe3..9934d50c3 100644 --- a/surfsense_web/app/api/contact/route.ts +++ b/surfsense_web/app/api/contact/route.ts @@ -1,4 +1,5 @@ import { type NextRequest, NextResponse } from "next/server"; +import { logger } from "@/lib/logger"; import { z } from "zod"; import { db } from "@/app/db"; import { usersTable } from "@/app/db/schema"; @@ -49,7 +50,7 @@ export async function POST(request: NextRequest) { ); } - console.error("Error submitting contact form:", error); + logger.error("Error submitting contact form:", error); return NextResponse.json( { success: false, diff --git a/surfsense_web/app/api/convert-to-blocknote/route.ts b/surfsense_web/app/api/convert-to-blocknote/route.ts index e11c9cb47..d9b37c289 100644 --- a/surfsense_web/app/api/convert-to-blocknote/route.ts +++ b/surfsense_web/app/api/convert-to-blocknote/route.ts @@ -1,4 +1,5 @@ import { ServerBlockNoteEditor } from "@blocknote/server-util"; +import { logger } from "@/lib/logger"; import { type NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -10,11 +11,11 @@ export async function POST(request: NextRequest) { } // Log raw markdown input before conversion - // console.log(`\n${"=".repeat(80)}`); - // console.log("RAW MARKDOWN INPUT (BEFORE CONVERSION):"); - // console.log("=".repeat(80)); - // console.log(markdown); - // console.log(`${"=".repeat(80)}\n`); + // logger.info(`\n${"=".repeat(80)}`); + // logger.info("RAW MARKDOWN INPUT (BEFORE CONVERSION):"); + // logger.info("=".repeat(80)); + // logger.info(markdown); + // logger.info(`${"=".repeat(80)}\n`); // Create server-side editor instance const editor = ServerBlockNoteEditor.create(); @@ -28,7 +29,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ blocknote_document: blocks }); } catch (error: any) { - console.error("Failed to convert markdown to BlockNote:", error); + logger.error("Failed to convert markdown to BlockNote:", error); return NextResponse.json( { error: "Failed to convert markdown to BlockNote blocks", diff --git a/surfsense_web/app/api/convert-to-markdown/route.ts b/surfsense_web/app/api/convert-to-markdown/route.ts index 7005a800f..00f49c444 100644 --- a/surfsense_web/app/api/convert-to-markdown/route.ts +++ b/surfsense_web/app/api/convert-to-markdown/route.ts @@ -1,4 +1,5 @@ import { ServerBlockNoteEditor } from "@blocknote/server-util"; +import { logger } from "@/lib/logger"; import { type NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -19,7 +20,7 @@ export async function POST(request: NextRequest) { markdown, }); } catch (error) { - console.error("Failed to convert BlockNote to markdown:", error); + logger.error("Failed to convert BlockNote to markdown:", error); return NextResponse.json( { error: "Failed to convert BlockNote blocks to markdown" }, { status: 500 } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx index f107ffa6c..45a89f629 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx @@ -1,6 +1,7 @@ "use client"; import { format } from "date-fns"; +import { logger } from "@/lib/logger"; import { Calendar as CalendarIcon, Clock, @@ -108,7 +109,7 @@ export default function ConnectorsPage() { useEffect(() => { if (error) { toast.error(t("failed_load")); - console.error("Error fetching connectors:", error); + logger.error("Error fetching connectors:", error); } }, [error, t]); @@ -120,7 +121,7 @@ export default function ConnectorsPage() { await deleteConnector(connectorToDelete); toast.success(t("delete_success")); } catch (error) { - console.error("Error deleting connector:", error); + logger.error("Error deleting connector:", error); toast.error(t("delete_failed")); } finally { setConnectorToDelete(null); @@ -147,7 +148,7 @@ export default function ConnectorsPage() { await indexConnector(selectedConnectorForIndexing, searchSpaceId, startDateStr, endDateStr); toast.success(t("indexing_started")); } catch (error) { - console.error("Error indexing connector content:", error); + logger.error("Error indexing connector content:", error); toast.error(error instanceof Error ? error.message : t("indexing_failed")); } finally { setIndexingConnectorId(null); @@ -164,7 +165,7 @@ export default function ConnectorsPage() { await indexConnector(connectorId, searchSpaceId); toast.success(t("indexing_started")); } catch (error) { - console.error("Error indexing connector content:", error); + logger.error("Error indexing connector content:", error); toast.error(error instanceof Error ? error.message : t("indexing_failed")); } finally { setIndexingConnectorId(null); @@ -233,7 +234,7 @@ export default function ConnectorsPage() { ); setPeriodicDialogOpen(false); } catch (error) { - console.error("Error updating periodic indexing:", error); + logger.error("Error updating periodic indexing:", error); toast.error(error instanceof Error ? error.message : "Failed to update periodic indexing"); } finally { setIsSavingPeriodic(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx index 07578b874..175b773a2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -91,7 +92,7 @@ export default function EditConnectorPage() { const [connector, setConnector] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); - // console.log("connector", connector); + // logger.info("connector", connector); // Initialize the form const form = useForm({ resolver: zodResolver(apiConnectorFormSchema), @@ -154,7 +155,7 @@ export default function EditConnectorPage() { toast.success("Connector updated successfully!"); router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error updating connector:", error); + logger.error("Error updating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to update connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx index cc4330203..2580d5fa8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/airtable-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react"; +import { logger } from "@/lib/logger"; import { motion } from "motion/react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; @@ -61,7 +62,7 @@ export default function AirtableConnectorPage() { // Redirect to Airtable for authentication window.location.href = data.auth_url; } catch (error) { - console.error("Error connecting to Airtable:", error); + logger.error("Error connecting to Airtable:", error); toast.error("Failed to connect to Airtable"); } finally { setIsConnecting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/baidu-search-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/baidu-search-api/page.tsx index 3e9f4898e..b2ba62d21 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/baidu-search-api/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/baidu-search-api/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -114,7 +115,7 @@ export default function BaiduSearchApiPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx index 2d5f6954c..1209c67f2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/clickup-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, ExternalLink, Eye, EyeOff } from "lucide-react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; @@ -77,7 +78,7 @@ export default function ClickUpConnectorPage() { toast.success("ClickUp connector created successfully!"); router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating ClickUp connector:", error); + logger.error("Error creating ClickUp connector:", error); toast.error("Failed to create ClickUp connector. Please try again."); } finally { setIsLoading(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx index c625f8900..f7a53a5f0 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/confluence-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -100,7 +101,7 @@ export default function ConfluenceConnectorPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx index 1daa6bcd0..df5f61426 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -92,7 +93,7 @@ export default function DiscordConnectorPage() { toast.success("Discord connector created successfully!"); router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/elasticsearch-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/elasticsearch-connector/page.tsx index e417995ed..5eb13dd6b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/elasticsearch-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/elasticsearch-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import * as RadioGroup from "@radix-ui/react-radio-group"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; @@ -190,7 +191,7 @@ export default function ElasticsearchConnectorPage() { toast.success("Elasticsearch connector created successfully!"); router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx index 833d716a8..70eb655a0 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, CircleAlert, Github, Info, ListChecks, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -121,7 +122,7 @@ export default function GithubConnectorPage() { setStep("select_repos"); // Move to the next step toast.success(`Found ${data.length} repositories.`); } catch (error) { - console.error("Error fetching GitHub repositories:", error); + logger.error("Error fetching GitHub repositories:", error); const errorMessage = error instanceof Error ? error.message @@ -161,7 +162,7 @@ export default function GithubConnectorPage() { toast.success("GitHub connector created successfully!"); router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating GitHub connector:", error); + logger.error("Error creating GitHub connector:", error); const errorMessage = error instanceof Error ? error.message : "Failed to create GitHub connector."; toast.error(errorMessage); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx index 8179fbabc..8604ba339 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-calendar-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import Link from "next/link"; @@ -66,7 +67,7 @@ export default function GoogleCalendarConnectorPage() { // Redirect to Google for authentication window.location.href = data.auth_url; } catch (error) { - console.error("Error connecting to Google:", error); + logger.error("Error connecting to Google:", error); toast.error("Failed to connect to Google Calendar"); } finally { setIsConnecting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx index 8659d937c..79de7b5b1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/google-gmail-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import Link from "next/link"; @@ -65,7 +66,7 @@ export default function GoogleGmailConnectorPage() { // Redirect to Google for authentication window.location.href = data.auth_url; } catch (error) { - console.error("Error connecting to Google:", error); + logger.error("Error connecting to Google:", error); toast.error("Failed to connect to Google Gmail"); } finally { setIsConnecting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx index 6f4e31114..59553ff6c 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -113,7 +114,7 @@ export default function JiraConnectorPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx index 13df9a910..a55ee908a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -98,7 +99,7 @@ export default function LinearConnectorPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx index c20c2b576..c97602751 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -86,7 +87,7 @@ export default function LinkupApiPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx index 7d4b82b68..965f277ef 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/luma-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Key, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import Link from "next/link"; @@ -82,7 +83,7 @@ export default function LumaConnectorPage() { } }) .catch((error) => { - console.error("Error fetching connectors:", error); + logger.error("Error fetching connectors:", error); }); }, [fetchConnectors, searchSpaceId]); @@ -111,7 +112,7 @@ export default function LumaConnectorPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx index 310c31811..e96716c0b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -93,7 +94,7 @@ export default function NotionConnectorPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/searxng/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/searxng/page.tsx index 9645a3657..6e7512101 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/searxng/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/searxng/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -132,7 +133,7 @@ export default function SearxngConnectorPage() { toast.success("SearxNG connector created successfully!"); router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating SearxNG connector:", error); + logger.error("Error creating SearxNG connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx index 2574c8f44..9c3332722 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -86,7 +87,7 @@ export default function SerperApiPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx index 314ed8442..161e9386c 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -93,7 +94,7 @@ export default function SlackConnectorPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx index 40b98ca5c..68bc11d8f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; @@ -86,7 +87,7 @@ export default function TavilyApiPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/webcrawler-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/webcrawler-connector/page.tsx index 8edc34728..97cddb5df 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/webcrawler-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/webcrawler-connector/page.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { ArrowLeft, Check, Globe, Loader2 } from "lucide-react"; import { motion } from "motion/react"; import Link from "next/link"; @@ -84,7 +85,7 @@ export default function WebcrawlerConnectorPage() { } }) .catch((error) => { - console.error("Error fetching connectors:", error); + logger.error("Error fetching connectors:", error); }); }, [fetchConnectors, searchSpaceId]); @@ -123,7 +124,7 @@ export default function WebcrawlerConnectorPage() { // Navigate back to connectors page router.push(`/dashboard/${searchSpaceId}/connectors`); } catch (error) { - console.error("Error creating connector:", error); + logger.error("Error creating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to create connector"); } finally { setIsSubmitting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx index 1c4d440e7..014927ded 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx @@ -1,6 +1,7 @@ "use client"; import { FileText, Pencil, Trash2 } from "lucide-react"; +import { logger } from "@/lib/logger"; import { motion } from "motion/react"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -43,7 +44,7 @@ export function RowActions({ else toast.error("Failed to delete document"); await refreshDocuments(); } catch (error) { - console.error("Error deleting document:", error); + logger.error("Error deleting document:", error); toast.error("Failed to delete document"); } finally { setIsDeleting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx index da07ca4af..8d37b4501 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx @@ -1,6 +1,7 @@ "use client"; import { motion } from "motion/react"; +import { logger } from "@/lib/logger"; import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useId, useMemo, useState } from "react"; @@ -135,7 +136,7 @@ export default function DocumentsTable() { await refreshCurrentView(); setSelectedIds(new Set()); } catch (e) { - console.error(e); + logger.error(e); toast.error(t("delete_error")); } }; diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index a7e0d6861..c713287ac 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -1,6 +1,7 @@ "use client"; import { AlertCircle, FileText, Loader2, Save, X } from "lucide-react"; +import { logger } from "@/lib/logger"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -35,7 +36,7 @@ export default function EditorPage() { async function fetchDocument() { const token = getBearerToken(); if (!token) { - console.error("No auth token found"); + logger.error("No auth token found"); // Redirect to login with current path saved redirectToLogin(); return; @@ -69,7 +70,7 @@ export default function EditorPage() { setEditorContent(data.blocknote_document); setError(null); } catch (error) { - console.error("Error fetching document:", error); + logger.error("Error fetching document:", error); setError( error instanceof Error ? error.message : "Failed to fetch document. Please try again." ); @@ -133,7 +134,7 @@ export default function EditorPage() { router.push(`/dashboard/${params.search_space_id}/documents`); }, 500); } catch (error) { - console.error("Error saving document:", error); + logger.error("Error saving document:", error); toast.error( error instanceof Error ? error.message : "Failed to save document. Please try again." ); diff --git a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx index 40a3e46a1..addca97cc 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx @@ -42,6 +42,7 @@ import { Zap, } from "lucide-react"; import { AnimatePresence, motion, type Variants } from "motion/react"; +import { logger } from "@/lib/logger"; import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; import React, { useContext, useId, useMemo, useRef, useState } from "react"; @@ -423,7 +424,7 @@ export default function LogsManagePage() { await refreshLogs(); table.resetRowSelection(); } catch (error: any) { - console.error("Error deleting logs:", error); + logger.error("Error deleting logs:", error); toast.error("Error deleting logs"); } }; @@ -1129,7 +1130,7 @@ function LogRowActions({ row, t }: { row: Row; t: (key: string) => string } // toast.success(t("log_deleted_success")); await refreshLogs(); } catch (error) { - console.error("Error deleting log:", error); + logger.error("Error deleting log:", error); toast.error(t("log_deleted_error")); } finally { setIsDeleting(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx index 7382429d2..66d5ff10a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx @@ -1,6 +1,7 @@ "use client"; import { FileText, MessageSquare, UserPlus, Users } from "lucide-react"; +import { logger } from "@/lib/logger"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -120,7 +121,7 @@ const OnboardPage = () => { }); } } catch (error) { - console.error("Auto-configuration failed:", error); + logger.error("Auto-configuration failed:", error); } finally { setIsAutoConfiguring(false); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx index 730defae8..09f4601a5 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx @@ -1,6 +1,7 @@ "use client"; import { format } from "date-fns"; +import { logger } from "@/lib/logger"; import { useAtom, useAtomValue } from "jotai"; import { Calendar, @@ -116,7 +117,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient if (isFetchingPodcasts) return; if (fetchError) { - console.error("Error fetching podcasts:", fetchError); + logger.error("Error fetching podcasts:", fetchError); setFilteredPodcasts([]); return; } @@ -324,7 +325,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient clearTimeout(timeoutId); } } catch (error) { - console.error("Error fetching or playing podcast:", error); + logger.error("Error fetching or playing podcast:", error); toast.error(error instanceof Error ? error.message : "Failed to load podcast audio."); // Reset state on error setCurrentPodcast(null); @@ -354,7 +355,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient setIsPlaying(false); } } catch (error) { - console.error("Error deleting podcast:", error); + logger.error("Error deleting podcast:", error); toast.error(error instanceof Error ? error.message : "Failed to delete podcast"); } }; @@ -923,7 +924,7 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient setIsPlaying(true); }) .catch((error) => { - console.error("Error playing audio:", error); + logger.error("Error playing audio:", error); // Don't show error if it's just the user navigating away if (error.name !== "AbortError") { toast.error("Failed to play audio."); @@ -936,10 +937,10 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient }} onEnded={() => setIsPlaying(false)} onError={(e) => { - console.error("Audio error:", e); + logger.error("Audio error:", e); if (audioRef.current?.error) { // Log the specific error code for debugging - console.error("Audio error code:", audioRef.current.error.code); + logger.error("Audio error code:", audioRef.current.error.code); // Don't show error message for aborted loads if (audioRef.current.error.code !== audioRef.current.error.MEDIA_ERR_ABORTED) { diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx index 1a9a607fb..c439c23f9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx @@ -1,6 +1,7 @@ "use client"; import { type CreateMessage, type Message, useChat } from "@ai-sdk/react"; +import { logger } from "@/lib/logger"; import { useAtomValue } from "jotai"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useRef } from "react"; @@ -17,14 +18,13 @@ export default function ResearcherPage() { const { search_space_id } = useParams(); const router = useRouter(); const hasSetInitialConnectors = useRef(false); - const hasInitiatedResponse = useRef(null); const activeChatId = useAtomValue(activeChatIdAtom); const { data: activeChatState, isFetching: isChatLoading } = useAtomValue(activeChatAtom); const { mutateAsync: createChat } = useAtomValue(createChatMutationAtom); const { mutateAsync: updateChat } = useAtomValue(updateChatMutationAtom); const isNewChat = !activeChatId; - // Reset the flag when chat ID changes (but not hasInitiatedResponse - we need to remember if we already initiated) + // Reset the flag when chat ID changes useEffect(() => { hasSetInitialConnectors.current = false; }, [activeChatId]); @@ -93,7 +93,7 @@ export default function ResearcherPage() { try { return JSON.parse(stored); } catch (error) { - console.error("Error parsing stored chat state:", error); + logger.error("Error parsing stored chat state:", error); return null; } } @@ -118,7 +118,7 @@ export default function ResearcherPage() { }, }, onError: (error) => { - console.error("Chat error:", error); + logger.error("Chat error:", error); }, }); @@ -168,14 +168,10 @@ export default function ResearcherPage() { if (chatData.messages && Array.isArray(chatData.messages)) { if (chatData.messages.length === 1 && chatData.messages[0].role === "user") { // Single user message - append to trigger LLM response - // Only if we haven't already initiated for this chat and handler doesn't have messages yet - if (hasInitiatedResponse.current !== activeChatId && handler.messages.length === 0) { - hasInitiatedResponse.current = activeChatId; - handler.append({ - role: "user", - content: chatData.messages[0].content, - }); - } + handler.append({ + role: "user", + content: chatData.messages[0].content, + }); } else if (chatData.messages.length > 1) { // Multiple messages - set them all handler.setMessages(chatData.messages); diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index dd3f25218..76feb318e 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -41,6 +41,7 @@ import { X, } from "lucide-react"; import { motion } from "motion/react"; +import { logger } from "@/lib/logger"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; @@ -970,7 +971,7 @@ function CreateInviteDialog({ const invite = await onCreateInvite(data); setCreatedInvite(invite); } catch (error) { - console.error("Failed to create invite:", error); + logger.error("Failed to create invite:", error); } finally { setCreating(false); } @@ -1187,7 +1188,7 @@ function CreateRoleDialog({ setSelectedPermissions([]); setIsDefault(false); } catch (error) { - console.error("Failed to create role:", error); + logger.error("Failed to create role:", error); } finally { setCreating(false); } diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index 0b3450d20..aedc2d7e5 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -1,6 +1,7 @@ "use client"; import { AlertCircle, Loader2, Plus, Search, Trash2, UserCheck, Users } from "lucide-react"; +import { logger } from "@/lib/logger"; import { motion, type Variants } from "motion/react"; import Image from "next/image"; import Link from "next/link"; @@ -187,7 +188,7 @@ const DashboardPage = () => { // Refresh the search spaces list after successful deletion refreshSearchSpaces(); } catch (error) { - console.error("Error deleting search space:", error); + logger.error("Error deleting search space:", error); toast.error("An error occurred while deleting the search space"); return; } diff --git a/surfsense_web/app/dashboard/searchspaces/page.tsx b/surfsense_web/app/dashboard/searchspaces/page.tsx index 520c4358e..84d1294bd 100644 --- a/surfsense_web/app/dashboard/searchspaces/page.tsx +++ b/surfsense_web/app/dashboard/searchspaces/page.tsx @@ -1,6 +1,7 @@ "use client"; import { motion } from "motion/react"; +import { logger } from "@/lib/logger"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { SearchSpaceForm } from "@/components/search-space-form"; @@ -38,7 +39,7 @@ export default function SearchSpacesPage() { return result; } catch (error) { - console.error("Error creating search space:", error); + logger.error("Error creating search space:", error); throw error; } }; diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx index 70119dfe4..d7fe6fdef 100644 --- a/surfsense_web/components/TokenHandler.tsx +++ b/surfsense_web/components/TokenHandler.tsx @@ -1,6 +1,7 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; +import { logger } from "@/lib/logger"; import { useEffect } from "react"; import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils"; @@ -49,7 +50,7 @@ const TokenHandler = ({ // Redirect to the appropriate path router.push(finalRedirectPath); } catch (error) { - console.error("Error storing token in localStorage:", error); + logger.error("Error storing token in localStorage:", error); // Even if there's an error, try to redirect to the default path router.push(redirectPath); } diff --git a/surfsense_web/components/UserDropdown.tsx b/surfsense_web/components/UserDropdown.tsx index 230bf0554..47a0ed092 100644 --- a/surfsense_web/components/UserDropdown.tsx +++ b/surfsense_web/components/UserDropdown.tsx @@ -3,6 +3,7 @@ import { BadgeCheck, LogOut, Settings } from "lucide-react"; import { useRouter } from "next/navigation"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { logger } from "@/lib/logger"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -32,7 +33,7 @@ export function UserDropdown({ router.push("/"); } } catch (error) { - console.error("Error during logout:", error); + logger.error("Error during logout:", error); // Optionally, provide user feedback if (typeof window !== "undefined") { alert("Logout failed. Please try again."); diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx index cb0fcb33e..31b89a7f8 100644 --- a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx @@ -1,6 +1,7 @@ "use client"; import { useAtomValue } from "jotai"; import { LoaderIcon, TriangleAlert } from "lucide-react"; +import { logger } from "@/lib/logger"; import { toast } from "sonner"; import { activeChatAtom } from "@/atoms/chats/chat-query.atoms"; import { activeChathatUIAtom, activeChatIdAtom } from "@/atoms/chats/ui.atoms"; @@ -27,7 +28,7 @@ export function ChatPanelContainer() { toast.success(`Podcast generation started!`); } catch (error) { toast.error("Error generating podcast. Please try again later."); - console.error("Error generating podcast:", JSON.stringify(generatePodcastError)); + logger.error("Error generating podcast:", JSON.stringify(generatePodcastError)); } }; diff --git a/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayer.tsx b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayer.tsx index 63bd22c37..5559c3cd7 100644 --- a/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayer.tsx +++ b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayer.tsx @@ -1,6 +1,7 @@ "use client"; import { Pause, Play, SkipBack, SkipForward, Volume2, VolumeX, X } from "lucide-react"; +import { logger } from "@/lib/logger"; import { motion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; @@ -84,7 +85,7 @@ export function PodcastPlayer({ clearTimeout(timeoutId); } } catch (error) { - console.error("Error fetching podcast:", error); + logger.error("Error fetching podcast:", error); toast.error(error instanceof Error ? error.message : "Failed to load podcast audio."); setAudioSrc(undefined); } finally { @@ -309,9 +310,9 @@ export function PodcastPlayer({ onLoadedMetadata={handleMetadataLoaded} onEnded={() => setIsPlaying(false)} onError={(e) => { - console.error("Audio error:", e); + logger.error("Audio error:", e); if (audioRef.current?.error) { - console.error("Audio error code:", audioRef.current.error.code); + logger.error("Audio error code:", audioRef.current.error.code); if (audioRef.current.error.code !== audioRef.current.error.MEDIA_ERR_ABORTED) { toast.error("Error playing audio. Please try again."); } diff --git a/surfsense_web/components/chat/CodeBlock.tsx b/surfsense_web/components/chat/CodeBlock.tsx index 7641a8b82..58b95b403 100644 --- a/surfsense_web/components/chat/CodeBlock.tsx +++ b/surfsense_web/components/chat/CodeBlock.tsx @@ -1,6 +1,7 @@ "use client"; import { Check, Copy } from "lucide-react"; +import { logger } from "@/lib/logger"; import { useTheme } from "next-themes"; import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; @@ -115,7 +116,7 @@ export const CodeBlock = memo(({ children, language }) => { const timeoutId = setTimeout(() => setCopied(false), COPY_TIMEOUT); return () => clearTimeout(timeoutId); } catch (error) { - console.warn("Failed to copy code to clipboard:", error); + logger.warn("Failed to copy code to clipboard:", error); } }, [children]); diff --git a/surfsense_web/components/contact/contact-form.tsx b/surfsense_web/components/contact/contact-form.tsx index 435ff1365..8540faa33 100644 --- a/surfsense_web/components/contact/contact-form.tsx +++ b/surfsense_web/components/contact/contact-form.tsx @@ -1,5 +1,6 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { logger } from "@/lib/logger"; import { IconMailFilled } from "@tabler/icons-react"; import { motion } from "motion/react"; import Image from "next/image"; @@ -58,7 +59,7 @@ export function ContactFormGridWithDetails() { }); } } catch (error) { - console.error("Error submitting form:", error); + logger.error("Error submitting form:", error); toast.error("Something went wrong", { description: "Please try again later.", }); diff --git a/surfsense_web/components/json-metadata-viewer.tsx b/surfsense_web/components/json-metadata-viewer.tsx index 8fe1b10ae..d25360936 100644 --- a/surfsense_web/components/json-metadata-viewer.tsx +++ b/surfsense_web/components/json-metadata-viewer.tsx @@ -1,4 +1,5 @@ import { FileJson } from "lucide-react"; +import { logger } from "@/lib/logger"; import React from "react"; import { defaultStyles, JsonView } from "react-json-view-lite"; import { Button } from "@/components/ui/button"; @@ -38,7 +39,7 @@ export function JsonMetadataViewer({ // Otherwise, use it as is return metadata; } catch (error) { - console.error("Error parsing JSON metadata:", error); + logger.error("Error parsing JSON metadata:", error); return { error: "Invalid JSON metadata" }; } }, [metadata]); diff --git a/surfsense_web/components/onboard/setup-prompt-step.tsx b/surfsense_web/components/onboard/setup-prompt-step.tsx index 899d856fa..a1003e24c 100644 --- a/surfsense_web/components/onboard/setup-prompt-step.tsx +++ b/surfsense_web/components/onboard/setup-prompt-step.tsx @@ -1,6 +1,7 @@ "use client"; import { ChevronDown, ChevronUp, ExternalLink, Info, Sparkles, User } from "lucide-react"; +import { logger } from "@/lib/logger"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -97,7 +98,7 @@ export function SetupPromptStep({ searchSpaceId, onComplete }: SetupPromptStepPr setHasChanges(false); onComplete?.(); } catch (error: any) { - console.error("Error saving prompt configuration:", error); + logger.error("Error saving prompt configuration:", error); toast.error(error.message || "Failed to save prompt configuration"); } finally { setSaving(false); diff --git a/surfsense_web/components/settings/prompt-config-manager.tsx b/surfsense_web/components/settings/prompt-config-manager.tsx index a1199c10d..9ec786ec4 100644 --- a/surfsense_web/components/settings/prompt-config-manager.tsx +++ b/surfsense_web/components/settings/prompt-config-manager.tsx @@ -10,6 +10,7 @@ import { Sparkles, User, } from "lucide-react"; +import { logger } from "@/lib/logger"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -101,7 +102,7 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) // Refresh to get updated data await fetchSearchSpace(); } catch (error: any) { - console.error("Error saving prompt configuration:", error); + logger.error("Error saving prompt configuration:", error); toast.error(error.message || "Failed to save prompt configuration"); } finally { setSaving(false); diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index e100a4c33..2e9d36d08 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -5,6 +5,7 @@ import { Trash2 } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms"; +import { logger } from "@/lib/logger"; import { chatsAtom } from "@/atoms/chats/chat-query.atoms"; import { globalChatsQueryParamsAtom } from "@/atoms/chats/ui.atoms"; import { AppSidebar } from "@/components/sidebar/app-sidebar"; @@ -107,7 +108,7 @@ export function AppSidebarProvider({ try { await deleteChat({ id: chatToDelete.id }); } catch (error) { - console.error("Error deleting chat:", error); + logger.error("Error deleting chat:", error); // You could show a toast notification here } finally { setShowDeleteDialog(false); diff --git a/surfsense_web/hooks/use-api-key.ts b/surfsense_web/hooks/use-api-key.ts index a5f24d4c6..4cc7a125d 100644 --- a/surfsense_web/hooks/use-api-key.ts +++ b/surfsense_web/hooks/use-api-key.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { getBearerToken } from "@/lib/auth-utils"; +import { logger } from "@/lib/logger"; interface UseApiKeyReturn { apiKey: string | null; @@ -21,7 +22,7 @@ export function useApiKey(): UseApiKeyReturn { const token = getBearerToken(); setApiKey(token); } catch (error) { - console.error("Error loading API key:", error); + logger.error("Error loading API key:", error); toast.error("Failed to load API key"); } finally { setIsLoading(false); @@ -33,60 +34,19 @@ export function useApiKey(): UseApiKeyReturn { return () => clearTimeout(timer); }, []); - const fallbackCopyTextToClipboard = (text: string) => { - const textArea = document.createElement("textarea"); - textArea.value = text; - - // Avoid scrolling to bottom - textArea.style.top = "0"; - textArea.style.left = "0"; - textArea.style.position = "fixed"; - textArea.style.opacity = "0"; - - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - try { - const successful = document.execCommand("copy"); - document.body.removeChild(textArea); - - if (successful) { - setCopied(true); - toast.success("API key copied to clipboard"); - - setTimeout(() => { - setCopied(false); - }, 2000); - } else { - toast.error("Failed to copy API key"); - } - } catch (err) { - console.error("Fallback: Oops, unable to copy", err); - document.body.removeChild(textArea); - toast.error("Failed to copy API key"); - } - }; - const copyToClipboard = useCallback(async () => { if (!apiKey) return; try { - if (navigator.clipboard && window.isSecureContext) { - // Use Clipboard API if available and in secure context - await navigator.clipboard.writeText(apiKey); - setCopied(true); - toast.success("API key copied to clipboard"); + await navigator.clipboard.writeText(apiKey); + setCopied(true); + toast.success("API key copied to clipboard"); - setTimeout(() => { - setCopied(false); - }, 2000); - } else { - // Fallback for non-secure contexts or browsers without clipboard API - fallbackCopyTextToClipboard(apiKey); - } + setTimeout(() => { + setCopied(false); + }, 2000); } catch (err) { - console.error("Failed to copy:", err); + logger.error("Failed to copy:", err); toast.error("Failed to copy API key"); } }, [apiKey]); diff --git a/surfsense_web/hooks/use-community-prompts.ts b/surfsense_web/hooks/use-community-prompts.ts index 3b4ac59db..9d7aec5e4 100644 --- a/surfsense_web/hooks/use-community-prompts.ts +++ b/surfsense_web/hooks/use-community-prompts.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; export interface CommunityPrompt { key: string; @@ -31,7 +32,7 @@ export function useCommunityPrompts() { setError(null); } catch (err: any) { setError(err.message || "Failed to fetch community prompts"); - console.error("Error fetching community prompts:", err); + logger.error("Error fetching community prompts:", err); } finally { setLoading(false); } diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index 899cbb961..dda14c36a 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -1,6 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { @@ -196,7 +197,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) setNewSelectedRepos(currentSelectedRepos); toast.success(`Found ${data.length} repos.`); } catch (error) { - console.error("Error fetching GitHub repositories:", error); + logger.error("Error fetching GitHub repositories:", error); toast.error(error instanceof Error ? error.message : "Failed to fetch repositories."); } finally { setIsFetchingRepos(false); @@ -602,7 +603,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } // Resetting simple form values is handled by useEffect if connector state updates } catch (error) { - console.error("Error updating connector:", error); + logger.error("Error updating connector:", error); toast.error(error instanceof Error ? error.message : "Failed to update connector."); } finally { setIsSaving(false); diff --git a/surfsense_web/hooks/use-document-by-chunk.ts b/surfsense_web/hooks/use-document-by-chunk.ts index 630e810a2..96cc183ee 100644 --- a/surfsense_web/hooks/use-document-by-chunk.ts +++ b/surfsense_web/hooks/use-document-by-chunk.ts @@ -1,5 +1,6 @@ "use client"; import { useCallback, useState } from "react"; +import { logger } from "@/lib/logger"; import { toast } from "sonner"; import { authenticatedFetch } from "@/lib/auth-utils"; @@ -84,7 +85,7 @@ export function useDocumentByChunk() { } catch (err: any) { const errorMessage = err.message || "Failed to fetch document"; setError(errorMessage); - console.error("Error fetching document by chunk:", err); + logger.error("Error fetching document by chunk:", err); throw err; } finally { setLoading(false); diff --git a/surfsense_web/hooks/use-document-types.ts b/surfsense_web/hooks/use-document-types.ts index 21c9eb6fe..3bb0d5fcf 100644 --- a/surfsense_web/hooks/use-document-types.ts +++ b/surfsense_web/hooks/use-document-types.ts @@ -56,7 +56,7 @@ export const useDocumentTypes = (searchSpaceId?: number, lazy: boolean = false) return typeCounts; } catch (err) { setError(err instanceof Error ? err : new Error("An unknown error occurred")); - console.error("Error fetching document types:", err); + logger.error("Error fetching document types:", err); } finally { setIsLoading(false); } diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index b5c349091..86e434285 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -1,5 +1,6 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; import { toast } from "sonner"; import { authenticatedFetch } from "@/lib/auth-utils"; import { normalizeListResponse } from "@/lib/pagination"; @@ -97,7 +98,7 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption setIsLoaded(true); } catch (err: any) { setError(err.message || "Failed to fetch documents"); - console.error("Error fetching documents:", err); + logger.error("Error fetching documents:", err); } finally { setLoading(false); } @@ -172,7 +173,7 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption setError(null); } catch (err: any) { setError(err.message || "Failed to search documents"); - console.error("Error searching documents:", err); + logger.error("Error searching documents:", err); } finally { setLoading(false); } @@ -200,7 +201,7 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption return true; } catch (err: any) { toast.error(err.message || "Failed to delete document"); - console.error("Error deleting document:", err); + logger.error("Error deleting document:", err); return false; } }, @@ -226,7 +227,7 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption const counts = await response.json(); return counts as Record; } catch (err: any) { - console.error("Error fetching document type counts:", err); + logger.error("Error fetching document type counts:", err); return {}; } }, [searchSpaceId]); diff --git a/surfsense_web/hooks/use-github-stars.ts b/surfsense_web/hooks/use-github-stars.ts index a4d4f80fd..0c2cf283a 100644 --- a/surfsense_web/hooks/use-github-stars.ts +++ b/surfsense_web/hooks/use-github-stars.ts @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; export const useGithubStars = () => { const [stars, setStars] = useState(null); @@ -26,7 +27,7 @@ export const useGithubStars = () => { setStars(data?.stargazers_count); } catch (err) { if (err instanceof Error) { - console.error("Error fetching stars:", err); + logger.error("Error fetching stars:", err); setError(err.message); } } finally { diff --git a/surfsense_web/hooks/use-llm-configs.ts b/surfsense_web/hooks/use-llm-configs.ts index 7619cc3e4..8557aba8c 100644 --- a/surfsense_web/hooks/use-llm-configs.ts +++ b/surfsense_web/hooks/use-llm-configs.ts @@ -1,5 +1,6 @@ "use client"; import { useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; import { toast } from "sonner"; import { authenticatedFetch } from "@/lib/auth-utils"; @@ -76,7 +77,7 @@ export function useLLMConfigs(searchSpaceId: number | null) { setError(null); } catch (err: any) { setError(err.message || "Failed to fetch LLM configurations"); - console.error("Error fetching LLM configurations:", err); + logger.error("Error fetching LLM configurations:", err); } finally { setLoading(false); } @@ -108,7 +109,7 @@ export function useLLMConfigs(searchSpaceId: number | null) { return newConfig; } catch (err: any) { toast.error(err.message || "Failed to create LLM configuration"); - console.error("Error creating LLM configuration:", err); + logger.error("Error creating LLM configuration:", err); return null; } }; @@ -129,7 +130,7 @@ export function useLLMConfigs(searchSpaceId: number | null) { return true; } catch (err: any) { toast.error(err.message || "Failed to delete LLM configuration"); - console.error("Error deleting LLM configuration:", err); + logger.error("Error deleting LLM configuration:", err); return false; } }; @@ -159,7 +160,7 @@ export function useLLMConfigs(searchSpaceId: number | null) { return updatedConfig; } catch (err: any) { toast.error(err.message || "Failed to update LLM configuration"); - console.error("Error updating LLM configuration:", err); + logger.error("Error updating LLM configuration:", err); return null; } }; @@ -202,7 +203,7 @@ export function useLLMPreferences(searchSpaceId: number | null) { setError(null); } catch (err: any) { setError(err.message || "Failed to fetch LLM preferences"); - console.error("Error fetching LLM preferences:", err); + logger.error("Error fetching LLM preferences:", err); } finally { setLoading(false); } @@ -239,7 +240,7 @@ export function useLLMPreferences(searchSpaceId: number | null) { return true; } catch (err: any) { toast.error(err.message || "Failed to update LLM preferences"); - console.error("Error updating LLM preferences:", err); + logger.error("Error updating LLM preferences:", err); return false; } }; @@ -284,7 +285,7 @@ export function useGlobalLLMConfigs() { setError(null); } catch (err: any) { setError(err.message || "Failed to fetch global LLM configurations"); - console.error("Error fetching global LLM configurations:", err); + logger.error("Error fetching global LLM configurations:", err); } finally { setLoading(false); } diff --git a/surfsense_web/hooks/use-logs.ts b/surfsense_web/hooks/use-logs.ts index 6ce025e89..fc0db5548 100644 --- a/surfsense_web/hooks/use-logs.ts +++ b/surfsense_web/hooks/use-logs.ts @@ -112,7 +112,7 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { return data; } catch (err: any) { setError(err.message || "Failed to fetch logs"); - console.error("Error fetching logs:", err); + logger.error("Error fetching logs:", err); throw err; } finally { setLoading(false); @@ -163,7 +163,7 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { return newLog; } catch (err: any) { toast.error(err.message || "Failed to create log"); - console.error("Error creating log:", err); + logger.error("Error creating log:", err); throw err; } }, []); @@ -195,7 +195,7 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { return updatedLog; } catch (err: any) { toast.error(err.message || "Failed to update log"); - console.error("Error updating log:", err); + logger.error("Error updating log:", err); throw err; } }, @@ -220,7 +220,7 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { return true; } catch (err: any) { toast.error(err.message || "Failed to delete log"); - console.error("Error deleting log:", err); + logger.error("Error deleting log:", err); return false; } }, []); @@ -241,7 +241,7 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { return await response.json(); } catch (err: any) { toast.error(err.message || "Failed to fetch log"); - console.error("Error fetching log:", err); + logger.error("Error fetching log:", err); throw err; } }, []); @@ -286,7 +286,7 @@ export function useLogsSummary(searchSpaceId: number, hours: number = 24) { return data; } catch (err: any) { setError(err.message || "Failed to fetch logs summary"); - console.error("Error fetching logs summary:", err); + logger.error("Error fetching logs summary:", err); throw err; } finally { setLoading(false); diff --git a/surfsense_web/hooks/use-rbac.ts b/surfsense_web/hooks/use-rbac.ts index ee3450746..3e3802ea2 100644 --- a/surfsense_web/hooks/use-rbac.ts +++ b/surfsense_web/hooks/use-rbac.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { logger } from "@/lib/logger"; import { toast } from "sonner"; import { authenticatedFetch, getBearerToken, handleUnauthorized } from "@/lib/auth-utils"; @@ -122,7 +123,7 @@ export function useMembers(searchSpaceId: number) { return data; } catch (err: any) { setError(err.message || "Failed to fetch members"); - console.error("Error fetching members:", err); + logger.error("Error fetching members:", err); } finally { setLoading(false); } @@ -244,7 +245,7 @@ export function useRoles(searchSpaceId: number) { return data; } catch (err: any) { setError(err.message || "Failed to fetch roles"); - console.error("Error fetching roles:", err); + logger.error("Error fetching roles:", err); } finally { setLoading(false); } @@ -375,7 +376,7 @@ export function useInvites(searchSpaceId: number) { return data; } catch (err: any) { setError(err.message || "Failed to fetch invites"); - console.error("Error fetching invites:", err); + logger.error("Error fetching invites:", err); } finally { setLoading(false); } @@ -504,7 +505,7 @@ export function usePermissions() { return data.permissions; } catch (err: any) { setError(err.message || "Failed to fetch permissions"); - console.error("Error fetching permissions:", err); + logger.error("Error fetching permissions:", err); } finally { setLoading(false); } @@ -563,7 +564,7 @@ export function useUserAccess(searchSpaceId: number) { return data; } catch (err: any) { setError(err.message || "Failed to fetch access info"); - console.error("Error fetching access:", err); + logger.error("Error fetching access:", err); } finally { setLoading(false); } @@ -637,7 +638,7 @@ export function useInviteInfo(inviteCode: string | null) { return data; } catch (err: any) { setError(err.message || "Failed to fetch invite info"); - console.error("Error fetching invite info:", err); + logger.error("Error fetching invite info:", err); } finally { setLoading(false); } diff --git a/surfsense_web/hooks/use-search-source-connectors.ts b/surfsense_web/hooks/use-search-source-connectors.ts index 22c5b3553..f5d10ce69 100644 --- a/surfsense_web/hooks/use-search-source-connectors.ts +++ b/surfsense_web/hooks/use-search-source-connectors.ts @@ -1,5 +1,7 @@ import { useCallback, useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; import { authenticatedFetch, getBearerToken, handleUnauthorized } from "@/lib/auth-utils"; +import { DEFAULT_CONNECTORS, getApiConnectorId } from "@/lib/constants"; export interface SearchSourceConnector { id: number; @@ -95,7 +97,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: return data; } catch (err) { setError(err instanceof Error ? err : new Error("An unknown error occurred")); - console.error("Error fetching search source connectors:", err); + logger.error("Error fetching search source connectors:", err); } finally { setIsLoading(false); } @@ -121,36 +123,16 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: // Update connector source items when connectors change const updateConnectorSourceItems = (currentConnectors: SearchSourceConnector[]) => { // Start with the default hardcoded connectors - const defaultConnectors: ConnectorSourceItem[] = [ - { - id: 1, - name: "Crawled URL", - type: "CRAWLED_URL", - sources: [], - }, - { - id: 2, - name: "File", - type: "FILE", - sources: [], - }, - { - id: 3, - name: "Extension", - type: "EXTENSION", - sources: [], - }, - { - id: 4, - name: "Youtube Video", - type: "YOUTUBE_VIDEO", - sources: [], - }, - ]; + const defaultConnectors: ConnectorSourceItem[] = DEFAULT_CONNECTORS.map((connector) => ({ + id: connector.id, + name: connector.name, + type: connector.type, + sources: [], + })); // Add the API connectors const apiConnectors: ConnectorSourceItem[] = currentConnectors.map((connector, index) => ({ - id: 1000 + index, // Use a high ID to avoid conflicts with hardcoded IDs + id: getApiConnectorId(index), name: connector.name, type: connector.connector_type, sources: [], @@ -191,7 +173,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: updateConnectorSourceItems(updatedConnectors); return newConnector; } catch (err) { - console.error("Error creating search source connector:", err); + logger.error("Error creating search source connector:", err); throw err; } }; @@ -227,7 +209,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: updateConnectorSourceItems(updatedConnectors); return updatedConnector; } catch (err) { - console.error("Error updating search source connector:", err); + logger.error("Error updating search source connector:", err); throw err; } }; @@ -253,7 +235,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: setConnectors(updatedConnectors); updateConnectorSourceItems(updatedConnectors); } catch (err) { - console.error("Error deleting search source connector:", err); + logger.error("Error deleting search source connector:", err); throw err; } }; @@ -308,7 +290,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: return result; } catch (err) { - console.error("Error indexing connector content:", err); + logger.error("Error indexing connector content:", err); throw err; } }; diff --git a/surfsense_web/hooks/use-search-space.ts b/surfsense_web/hooks/use-search-space.ts index 849aad413..8432fca09 100644 --- a/surfsense_web/hooks/use-search-space.ts +++ b/surfsense_web/hooks/use-search-space.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; import { toast } from "sonner"; import { authenticatedFetch } from "@/lib/auth-utils"; @@ -44,7 +45,7 @@ export function useSearchSpace({ searchSpaceId, autoFetch = true }: UseSearchSpa setError(null); } catch (err: any) { setError(err.message || "Failed to fetch search space"); - console.error("Error fetching search space:", err); + logger.error("Error fetching search space:", err); } finally { setLoading(false); } diff --git a/surfsense_web/hooks/use-search-spaces.ts b/surfsense_web/hooks/use-search-spaces.ts index 03a87881c..51ce062ac 100644 --- a/surfsense_web/hooks/use-search-spaces.ts +++ b/surfsense_web/hooks/use-search-spaces.ts @@ -39,7 +39,7 @@ export function useSearchSpaces() { setError(null); } catch (err: any) { setError(err.message || "Failed to fetch search spaces"); - console.error("Error fetching search spaces:", err); + logger.error("Error fetching search spaces:", err); } finally { setLoading(false); } diff --git a/surfsense_web/hooks/use-user.ts b/surfsense_web/hooks/use-user.ts index e81ac350b..6bf456720 100644 --- a/surfsense_web/hooks/use-user.ts +++ b/surfsense_web/hooks/use-user.ts @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { logger } from "@/lib/logger"; import { toast } from "sonner"; import { authenticatedFetch } from "@/lib/auth-utils"; @@ -40,7 +41,7 @@ export function useUser() { setError(null); } catch (err: any) { setError(err.message || "Failed to fetch user"); - console.error("Error fetching user:", err); + logger.error("Error fetching user:", err); } finally { setLoading(false); } diff --git a/surfsense_web/lib/apis/auth-api.service.ts b/surfsense_web/lib/apis/auth-api.service.ts index df7d64721..eb05930d7 100644 --- a/surfsense_web/lib/apis/auth-api.service.ts +++ b/surfsense_web/lib/apis/auth-api.service.ts @@ -7,6 +7,7 @@ import { registerResponse, } from "@/contracts/types/auth.types"; import { ValidationError } from "../error"; +import { logger } from "@/lib/logger"; import { baseApiService } from "./base-api.service"; class AuthApiService { @@ -15,7 +16,7 @@ class AuthApiService { const parsedRequest = loginRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); @@ -41,7 +42,7 @@ class AuthApiService { const parsedRequest = registerRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index 3013be70a..60532b752 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -1,4 +1,5 @@ import type z from "zod"; +import { logger } from "@/lib/logger"; import { getBearerToken, handleUnauthorized } from "../auth-utils"; import { AppError, AuthenticationError, AuthorizationError, NotFoundError } from "../error"; @@ -121,7 +122,7 @@ class BaseApiService { try { data = await response.json(); } catch (error) { - console.error("Failed to parse response as JSON: ", JSON.stringify(error)); + logger.error("Failed to parse response as JSON: ", JSON.stringify(error)); throw new AppError("Failed to parse response", response.status, response.statusText); } @@ -176,7 +177,7 @@ class BaseApiService { data = await response.json(); } } catch (error) { - console.error("Failed to parse response as JSON:", error); + logger.error("Failed to parse response as JSON:", error); throw new AppError("Failed to parse response", response.status, response.statusText); } @@ -192,7 +193,7 @@ class BaseApiService { * This is a client side error, and should be fixed by updating the responseSchema to keep things typed. * This error should not be shown to the user , it is for dev only. */ - console.error(`Invalid API response schema - ${url} :`, JSON.stringify(parsedData.error)); + logger.error(`Invalid API response schema - ${url} :`, JSON.stringify(parsedData.error)); } return data; @@ -200,7 +201,7 @@ class BaseApiService { return data; } catch (error) { - console.error("Request failed:", JSON.stringify(error)); + logger.error("Request failed:", JSON.stringify(error)); throw error; } } diff --git a/surfsense_web/lib/apis/chats-api.service.ts b/surfsense_web/lib/apis/chats-api.service.ts index 7c53815a4..738413f07 100644 --- a/surfsense_web/lib/apis/chats-api.service.ts +++ b/surfsense_web/lib/apis/chats-api.service.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { logger } from "@/lib/logger"; import { type CreateChatRequest, chatDetails, @@ -23,7 +24,7 @@ class ChatApiService { const parsedRequest = getChatDetailsRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); @@ -38,7 +39,7 @@ class ChatApiService { const parsedRequest = getChatsRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); @@ -64,7 +65,7 @@ class ChatApiService { const parsedRequest = deleteChatRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); @@ -79,7 +80,7 @@ class ChatApiService { const parsedRequest = createChatRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); @@ -101,7 +102,7 @@ class ChatApiService { const parsedRequest = updateChatRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); diff --git a/surfsense_web/lib/apis/podcasts-api.service.ts b/surfsense_web/lib/apis/podcasts-api.service.ts index 733499a40..d1484f72f 100644 --- a/surfsense_web/lib/apis/podcasts-api.service.ts +++ b/surfsense_web/lib/apis/podcasts-api.service.ts @@ -1,4 +1,5 @@ import z from "zod"; +import { logger } from "@/lib/logger"; import { type DeletePodcastRequest, deletePodcastRequest, @@ -23,9 +24,9 @@ class PodcastsApiService { const parsedRequest = getPodcastsRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); - // Format a user frendly error message + // Format a user friendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); throw new ValidationError(`Invalid request: ${errorMessage}`); } @@ -49,9 +50,9 @@ class PodcastsApiService { const parsedRequest = getPodcastByChatIdRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); - // Format a user frendly error message + // Format a user friendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); throw new ValidationError(`Invalid request: ${errorMessage}`); } @@ -67,9 +68,9 @@ class PodcastsApiService { const parsedRequest = generatePodcastRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); - // Format a user frendly error message + // Format a user friendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); throw new ValidationError(`Invalid request: ${errorMessage}`); } @@ -90,9 +91,9 @@ class PodcastsApiService { const parsedRequest = loadPodcastRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); - // Format a user frendly error message + // Format a user friendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); throw new ValidationError(`Invalid request: ${errorMessage}`); } @@ -107,9 +108,9 @@ class PodcastsApiService { const parsedRequest = deletePodcastRequest.safeParse(request); if (!parsedRequest.success) { - console.error("Invalid request:", parsedRequest.error); + logger.error("Invalid request:", parsedRequest.error); - // Format a user frendly error message + // Format a user friendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); throw new ValidationError(`Invalid request: ${errorMessage}`); } diff --git a/surfsense_web/lib/constants.ts b/surfsense_web/lib/constants.ts new file mode 100644 index 000000000..29f747254 --- /dev/null +++ b/surfsense_web/lib/constants.ts @@ -0,0 +1,74 @@ +/** + * Application-wide constants for SurfSense web app. + * + * These values are extracted from hardcoded values throughout the codebase + * to ensure consistency and make them easier to maintain. + * + * IMPORTANT: Changing these values may affect existing data. Ensure backward + * compatibility when modifying. + */ + +/** + * Connector ID constants. + * + * Default connectors use IDs 1-99 (reserved range). + * API/dynamic connectors start from API_CONNECTOR_ID_OFFSET to avoid conflicts. + * + * These IDs are used for UI identification purposes, not database IDs. + */ +export const CONNECTOR_IDS = { + /** ID for Crawled URL connector */ + CRAWLED_URL: 1, + /** ID for File upload connector */ + FILE: 2, + /** ID for Browser Extension connector */ + EXTENSION: 3, + /** ID for YouTube Video connector */ + YOUTUBE_VIDEO: 4, + + /** + * Starting offset for API/dynamic connectors. + * API connectors use IDs starting from this value to avoid conflicts + * with hardcoded default connector IDs (1-99 reserved range). + * + * Example: First API connector gets ID 1000, second gets 1001, etc. + */ + API_CONNECTOR_ID_OFFSET: 1000, +} as const; + +/** + * Default connector configurations. + * These are the built-in connectors that are always available. + */ +export const DEFAULT_CONNECTORS = [ + { + id: CONNECTOR_IDS.CRAWLED_URL, + name: "Crawled URL", + type: "CRAWLED_URL", + }, + { + id: CONNECTOR_IDS.FILE, + name: "File", + type: "FILE", + }, + { + id: CONNECTOR_IDS.EXTENSION, + name: "Extension", + type: "EXTENSION", + }, + { + id: CONNECTOR_IDS.YOUTUBE_VIDEO, + name: "YouTube Video", + type: "YOUTUBE_VIDEO", + }, +] as const; + +/** + * Generates an ID for an API connector based on its index. + * + * @param index - The index of the connector in the API connectors list + * @returns A unique ID that doesn't conflict with default connector IDs + */ +export const getApiConnectorId = (index: number): number => { + return CONNECTOR_IDS.API_CONNECTOR_ID_OFFSET + index; +}; diff --git a/surfsense_web/lib/logger.ts b/surfsense_web/lib/logger.ts new file mode 100644 index 000000000..389570140 --- /dev/null +++ b/surfsense_web/lib/logger.ts @@ -0,0 +1,71 @@ +/** + * Environment-aware logger utility for SurfSense web application. + * + * In development mode: Uses console.log for debugging + * In production mode: Suppresses debug logs, only shows warnings and errors + * In test environment (NODE_ENV === 'test'): Logs may be suppressed or mocked + * depending on the test setup configuration. + * + * Usage: + * import { logger } from '@/lib/logger'; + * logger.debug('Debug message', data); + * logger.info('Info message'); + * logger.warn('Warning message'); + * logger.error('Error message', error); + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +interface LoggerConfig { + isDevelopment: boolean; + prefix: string; +} + +const config: LoggerConfig = { + isDevelopment: process.env.NODE_ENV !== 'production', + prefix: '[SurfSense]', +}; + +const formatMessage = (level: LogLevel, message: string): string => { + const timestamp = new Date().toISOString(); + return `${config.prefix} [${timestamp}] [${level.toUpperCase()}] ${message}`; +}; + +export const logger = { + /** + * Debug level logging - only shows in development + */ + debug: (message: string, ...args: unknown[]): void => { + if (config.isDevelopment) { + console.log(formatMessage('debug', message), ...args); + } + }, + + /** + * Info level logging - always shows (useful for tracking important operations) + */ + info: (message: string, ...args: unknown[]): void => { + console.info(formatMessage('info', message), ...args); + }, + + /** + * Warning level logging - always shows + */ + warn: (message: string, ...args: unknown[]): void => { + console.warn(formatMessage('warn', message), ...args); + }, + + /** + * Error level logging - always shows + */ + error: (message: string, ...args: unknown[]): void => { + console.error(formatMessage('error', message), ...args); + }, + + /** + * Check if we're in development mode + */ + isDev: (): boolean => config.isDevelopment, +}; + +export default logger; diff --git a/surfsense_web/tests/components/Logo.test.tsx b/surfsense_web/tests/components/Logo.test.tsx new file mode 100644 index 000000000..1c71a344f --- /dev/null +++ b/surfsense_web/tests/components/Logo.test.tsx @@ -0,0 +1,50 @@ +/** + * Tests for components/Logo.tsx + * + * These tests validate: + * 1. Logo renders as a link to home page + * 2. Logo image has correct alt text for accessibility + * 3. Logo accepts and applies custom className + */ + +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { Logo } from "@/components/Logo"; + +describe("Logo", () => { + it("should render a link to the home page", () => { + render(); + + const link = screen.getByRole("link"); + expect(link).toHaveAttribute("href", "/"); + }); + + it("should render an image with correct alt text", () => { + render(); + + const image = screen.getByAltText("logo"); + expect(image).toBeInTheDocument(); + }); + + it("should have correct image source", () => { + render(); + + const image = screen.getByAltText("logo"); + // Next.js Image component transforms the src, so we check if src attribute exists + expect(image).toHaveAttribute("src"); + }); + + it("should apply custom className to the image", () => { + render(); + + const image = screen.getByAltText("logo"); + expect(image).toHaveClass("h-8", "w-8"); + }); + + it("should render without className prop", () => { + render(); + + const image = screen.getByAltText("logo"); + expect(image).toBeInTheDocument(); + }); +}); diff --git a/surfsense_web/tests/components/ui/button.test.tsx b/surfsense_web/tests/components/ui/button.test.tsx new file mode 100644 index 000000000..9d7af9db5 --- /dev/null +++ b/surfsense_web/tests/components/ui/button.test.tsx @@ -0,0 +1,202 @@ +/** + * Tests for components/ui/button.tsx + * + * These tests validate: + * 1. Button renders correctly with different variants + * 2. Button renders correctly with different sizes + * 3. Button handles click events + * 4. Button supports asChild prop for composition + * 5. Button applies custom classNames correctly + */ + +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { Button, buttonVariants } from "@/components/ui/button"; + +describe("Button", () => { + describe("rendering", () => { + it("should render children correctly", () => { + render(); + + expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument(); + }); + + it("should render as a button element by default", () => { + render(); + + const button = screen.getByRole("button"); + expect(button.tagName).toBe("BUTTON"); + }); + + it("should apply data-slot attribute", () => { + render(); + + expect(screen.getByRole("button")).toHaveAttribute("data-slot", "button"); + }); + }); + + describe("variants", () => { + it("should apply default variant styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-primary"); + }); + + it("should apply destructive variant styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-destructive"); + }); + + it("should apply outline variant styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("border"); + expect(button).toHaveClass("bg-background"); + }); + + it("should apply secondary variant styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bg-secondary"); + }); + + it("should apply ghost variant styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("hover:bg-accent"); + }); + + it("should apply link variant styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("underline-offset-4"); + }); + }); + + describe("sizes", () => { + it("should apply default size styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("h-9"); + }); + + it("should apply small size styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("h-8"); + }); + + it("should apply large size styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("h-10"); + }); + + it("should apply icon size styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("size-9"); + }); + }); + + describe("interactions", () => { + it("should call onClick handler when clicked", () => { + const handleClick = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button")); + + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("should not call onClick when disabled", () => { + const handleClick = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByRole("button")); + + expect(handleClick).not.toHaveBeenCalled(); + }); + + it("should apply disabled styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + expect(button).toHaveClass("disabled:pointer-events-none"); + }); + }); + + describe("custom className", () => { + it("should merge custom className with default styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("custom-class"); + // Should still have base styles + expect(button).toHaveClass("inline-flex"); + }); + + it("should allow overriding default styles", () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("rounded-full"); + }); + }); + + describe("asChild prop", () => { + it("should render as child element when asChild is true", () => { + render( + + ); + + const link = screen.getByRole("link", { name: "Link Button" }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "/test"); + }); + }); +}); + +describe("buttonVariants", () => { + it("should be a function that returns className string", () => { + const className = buttonVariants(); + expect(typeof className).toBe("string"); + expect(className.length).toBeGreaterThan(0); + }); + + it("should generate different classes for different variants", () => { + const defaultClass = buttonVariants({ variant: "default" }); + const destructiveClass = buttonVariants({ variant: "destructive" }); + + expect(defaultClass).not.toBe(destructiveClass); + expect(defaultClass).toContain("bg-primary"); + expect(destructiveClass).toContain("bg-destructive"); + }); + + it("should generate different classes for different sizes", () => { + const defaultSize = buttonVariants({ size: "default" }); + const smallSize = buttonVariants({ size: "sm" }); + + expect(defaultSize).not.toBe(smallSize); + expect(defaultSize).toContain("h-9"); + expect(smallSize).toContain("h-8"); + }); +}); diff --git a/surfsense_web/tests/hooks/use-media-query.test.ts b/surfsense_web/tests/hooks/use-media-query.test.ts new file mode 100644 index 000000000..d399b8beb --- /dev/null +++ b/surfsense_web/tests/hooks/use-media-query.test.ts @@ -0,0 +1,199 @@ +/** + * Tests for hooks/use-media-query.ts and hooks/use-mobile.ts + * + * These tests validate: + * 1. Media query hook responds to viewport changes + * 2. Mobile detection works correctly at breakpoints + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { useIsMobile } from "@/hooks/use-mobile"; + +describe("useMediaQuery", () => { + let mockMatchMedia: ReturnType; + let mockAddEventListener: ReturnType; + let mockRemoveEventListener: ReturnType; + let changeHandler: ((event: MediaQueryListEvent) => void) | null = null; + + beforeEach(() => { + mockAddEventListener = vi.fn((event, handler) => { + if (event === "change") { + changeHandler = handler; + } + }); + mockRemoveEventListener = vi.fn(); + + mockMatchMedia = vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + + Object.defineProperty(window, "matchMedia", { + writable: true, + value: mockMatchMedia, + }); + + changeHandler = null; + }); + + it("should return false by default", () => { + const { result } = renderHook(() => useMediaQuery("(min-width: 768px)")); + + expect(result.current).toBe(false); + }); + + it("should return true when media query matches", () => { + mockMatchMedia.mockImplementation((query: string) => ({ + matches: true, + media: query, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + })); + + const { result } = renderHook(() => useMediaQuery("(min-width: 768px)")); + + expect(result.current).toBe(true); + }); + + it("should add event listener on mount", () => { + renderHook(() => useMediaQuery("(min-width: 768px)")); + + expect(mockAddEventListener).toHaveBeenCalledWith("change", expect.any(Function)); + }); + + it("should remove event listener on unmount", () => { + const { unmount } = renderHook(() => useMediaQuery("(min-width: 768px)")); + + unmount(); + + expect(mockRemoveEventListener).toHaveBeenCalledWith("change", expect.any(Function)); + }); + + it("should update when media query changes", () => { + const { result } = renderHook(() => useMediaQuery("(min-width: 768px)")); + + expect(result.current).toBe(false); + + // Simulate media query change + act(() => { + if (changeHandler) { + changeHandler({ matches: true } as MediaQueryListEvent); + } + }); + + expect(result.current).toBe(true); + }); + + it("should re-subscribe when query changes", () => { + const { rerender } = renderHook(({ query }) => useMediaQuery(query), { + initialProps: { query: "(min-width: 768px)" }, + }); + + expect(mockMatchMedia).toHaveBeenCalledWith("(min-width: 768px)"); + + rerender({ query: "(min-width: 1024px)" }); + + expect(mockMatchMedia).toHaveBeenCalledWith("(min-width: 1024px)"); + }); +}); + +describe("useIsMobile", () => { + let mockMatchMedia: ReturnType; + let mockAddEventListener: ReturnType; + let mockRemoveEventListener: ReturnType; + let changeHandler: (() => void) | null = null; + + beforeEach(() => { + mockAddEventListener = vi.fn((event, handler) => { + if (event === "change") { + changeHandler = handler; + } + }); + mockRemoveEventListener = vi.fn(); + + mockMatchMedia = vi.fn().mockImplementation(() => ({ + matches: false, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + })); + + Object.defineProperty(window, "matchMedia", { + writable: true, + value: mockMatchMedia, + }); + + // Default to desktop width + Object.defineProperty(window, "innerWidth", { + writable: true, + value: 1024, + }); + + changeHandler = null; + }); + + it("should return false for desktop width (>= 768px)", () => { + Object.defineProperty(window, "innerWidth", { value: 1024, writable: true }); + + const { result } = renderHook(() => useIsMobile()); + + expect(result.current).toBe(false); + }); + + it("should return true for mobile width (< 768px)", () => { + Object.defineProperty(window, "innerWidth", { value: 500, writable: true }); + + const { result } = renderHook(() => useIsMobile()); + + expect(result.current).toBe(true); + }); + + it("should return false at exactly 768px (breakpoint)", () => { + Object.defineProperty(window, "innerWidth", { value: 768, writable: true }); + + const { result } = renderHook(() => useIsMobile()); + + expect(result.current).toBe(false); + }); + + it("should return true at 767px (just below breakpoint)", () => { + Object.defineProperty(window, "innerWidth", { value: 767, writable: true }); + + const { result } = renderHook(() => useIsMobile()); + + expect(result.current).toBe(true); + }); + + it("should update when window is resized", () => { + Object.defineProperty(window, "innerWidth", { value: 1024, writable: true }); + + const { result } = renderHook(() => useIsMobile()); + + expect(result.current).toBe(false); + + // Simulate resize to mobile + act(() => { + Object.defineProperty(window, "innerWidth", { value: 500, writable: true }); + if (changeHandler) { + changeHandler(); + } + }); + + expect(result.current).toBe(true); + }); + + it("should clean up event listener on unmount", () => { + const { unmount } = renderHook(() => useIsMobile()); + + unmount(); + + expect(mockRemoveEventListener).toHaveBeenCalledWith("change", expect.any(Function)); + }); +}); diff --git a/surfsense_web/tests/lib/auth-utils.test.ts b/surfsense_web/tests/lib/auth-utils.test.ts new file mode 100644 index 000000000..2a931f692 --- /dev/null +++ b/surfsense_web/tests/lib/auth-utils.test.ts @@ -0,0 +1,210 @@ +/** + * Tests for lib/auth-utils.ts + * + * These tests validate: + * 1. Token storage and retrieval works correctly + * 2. Authentication state is properly tracked + * 3. Redirect path handling for post-login navigation + * 4. Auth headers are correctly constructed + */ + +import { describe, it, expect, beforeEach } from "vitest"; +import { + getBearerToken, + setBearerToken, + clearBearerToken, + isAuthenticated, + getAndClearRedirectPath, + getAuthHeaders, +} from "@/lib/auth-utils"; + +describe("Token Management", () => { + beforeEach(() => { + // Clear localStorage before each test + window.localStorage.clear(); + }); + + describe("getBearerToken", () => { + it("should return null when no token is stored", () => { + const token = getBearerToken(); + expect(token).toBeNull(); + }); + + it("should return the stored token", () => { + window.localStorage.setItem("surfsense_bearer_token", "test-token-123"); + + const token = getBearerToken(); + expect(token).toBe("test-token-123"); + }); + }); + + describe("setBearerToken", () => { + it("should store the token in localStorage", () => { + setBearerToken("my-auth-token"); + + expect(window.localStorage.getItem("surfsense_bearer_token")).toBe("my-auth-token"); + }); + + it("should overwrite existing token", () => { + setBearerToken("old-token"); + setBearerToken("new-token"); + + expect(window.localStorage.getItem("surfsense_bearer_token")).toBe("new-token"); + }); + }); + + describe("clearBearerToken", () => { + it("should remove the token from localStorage", () => { + window.localStorage.setItem("surfsense_bearer_token", "token-to-clear"); + + clearBearerToken(); + + expect(window.localStorage.getItem("surfsense_bearer_token")).toBeNull(); + }); + + it("should not throw when no token exists", () => { + expect(() => clearBearerToken()).not.toThrow(); + }); + }); +}); + +describe("Authentication State", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe("isAuthenticated", () => { + it("should return false when no token exists", () => { + expect(isAuthenticated()).toBe(false); + }); + + it("should return true when token exists", () => { + window.localStorage.setItem("surfsense_bearer_token", "valid-token"); + + expect(isAuthenticated()).toBe(true); + }); + + it("should return false after token is cleared", () => { + window.localStorage.setItem("surfsense_bearer_token", "valid-token"); + expect(isAuthenticated()).toBe(true); + + clearBearerToken(); + expect(isAuthenticated()).toBe(false); + }); + }); +}); + +describe("Redirect Path Handling", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe("getAndClearRedirectPath", () => { + it("should return null when no redirect path is stored", () => { + const path = getAndClearRedirectPath(); + expect(path).toBeNull(); + }); + + it("should return and clear stored redirect path", () => { + window.localStorage.setItem("surfsense_redirect_path", "/dashboard/settings"); + + const path = getAndClearRedirectPath(); + + expect(path).toBe("/dashboard/settings"); + expect(window.localStorage.getItem("surfsense_redirect_path")).toBeNull(); + }); + + it("should only return path once (cleared after first read)", () => { + window.localStorage.setItem("surfsense_redirect_path", "/some/path"); + + const firstRead = getAndClearRedirectPath(); + const secondRead = getAndClearRedirectPath(); + + expect(firstRead).toBe("/some/path"); + expect(secondRead).toBeNull(); + }); + }); +}); + +describe("Auth Headers", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe("getAuthHeaders", () => { + it("should return empty object when no token exists", () => { + const headers = getAuthHeaders(); + + expect(headers).toEqual({}); + }); + + it("should return Authorization header when token exists", () => { + window.localStorage.setItem("surfsense_bearer_token", "my-token"); + + const headers = getAuthHeaders(); + + expect(headers).toEqual({ + Authorization: "Bearer my-token", + }); + }); + + it("should merge additional headers with auth header", () => { + window.localStorage.setItem("surfsense_bearer_token", "my-token"); + + const headers = getAuthHeaders({ + "Content-Type": "application/json", + "X-Custom": "value", + }); + + expect(headers).toEqual({ + Authorization: "Bearer my-token", + "Content-Type": "application/json", + "X-Custom": "value", + }); + }); + + it("should return only additional headers when no token", () => { + const headers = getAuthHeaders({ + "Content-Type": "application/json", + }); + + expect(headers).toEqual({ + "Content-Type": "application/json", + }); + }); + + it("should handle undefined additional headers", () => { + window.localStorage.setItem("surfsense_bearer_token", "my-token"); + + const headers = getAuthHeaders(undefined); + + expect(headers).toEqual({ + Authorization: "Bearer my-token", + }); + }); + }); +}); + +describe("Token Format Validation", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + it("should handle tokens with special characters", () => { + const specialToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"; + + setBearerToken(specialToken); + const retrieved = getBearerToken(); + + expect(retrieved).toBe(specialToken); + }); + + it("should handle empty string token", () => { + setBearerToken(""); + const retrieved = getBearerToken(); + + expect(retrieved).toBe(""); + // Empty string is falsy, so isAuthenticated should return false + expect(isAuthenticated()).toBe(false); + }); +}); diff --git a/surfsense_web/tests/lib/pagination.test.ts b/surfsense_web/tests/lib/pagination.test.ts new file mode 100644 index 000000000..4bbbf8c23 --- /dev/null +++ b/surfsense_web/tests/lib/pagination.test.ts @@ -0,0 +1,166 @@ +/** + * Tests for lib/pagination.ts + * + * These tests validate: + * 1. normalizeListResponse correctly handles different API response formats + * 2. Edge cases and malformed data are handled gracefully + */ + +import { describe, it, expect } from "vitest"; +import { normalizeListResponse, type ListResponse } from "@/lib/pagination"; + +describe("normalizeListResponse", () => { + describe("Case 1: Already in desired shape { items, total }", () => { + it("should pass through correctly shaped response", () => { + const payload = { + items: [{ id: 1 }, { id: 2 }], + total: 10, + }; + + const result = normalizeListResponse<{ id: number }>(payload); + + expect(result.items).toEqual([{ id: 1 }, { id: 2 }]); + expect(result.total).toBe(10); + }); + + it("should use items length if total is missing", () => { + const payload = { + items: [{ id: 1 }, { id: 2 }, { id: 3 }], + }; + + const result = normalizeListResponse(payload); + + expect(result.items.length).toBe(3); + expect(result.total).toBe(3); + }); + + it("should handle empty items array", () => { + const payload = { + items: [], + total: 0, + }; + + const result = normalizeListResponse(payload); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + }); + + describe("Case 2: Tuple format [items, total]", () => { + it("should normalize tuple response", () => { + const payload = [[{ id: 1 }, { id: 2 }], 100]; + + const result = normalizeListResponse<{ id: number }>(payload); + + expect(result.items).toEqual([{ id: 1 }, { id: 2 }]); + expect(result.total).toBe(100); + }); + + it("should use items length if total is not a number in tuple", () => { + const payload = [[{ id: 1 }, { id: 2 }], "invalid"]; + + const result = normalizeListResponse(payload); + + expect(result.items.length).toBe(2); + expect(result.total).toBe(2); + }); + + it("should handle empty tuple array", () => { + const payload = [[], 0]; + + const result = normalizeListResponse(payload); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + }); + + describe("Case 3: Plain array", () => { + it("should normalize plain array response", () => { + const payload = [{ id: 1 }, { id: 2 }, { id: 3 }]; + + const result = normalizeListResponse<{ id: number }>(payload); + + expect(result.items).toEqual(payload); + expect(result.total).toBe(3); + }); + + it("should handle empty plain array", () => { + const payload: unknown[] = []; + + const result = normalizeListResponse(payload); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + }); + + describe("Edge cases and error handling", () => { + it("should return empty result for null payload", () => { + const result = normalizeListResponse(null); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + + it("should return empty result for undefined payload", () => { + const result = normalizeListResponse(undefined); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + + it("should return empty result for string payload", () => { + const result = normalizeListResponse("invalid"); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + + it("should return empty result for number payload", () => { + const result = normalizeListResponse(123); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + + it("should return empty result for object without items", () => { + const result = normalizeListResponse({ data: [1, 2, 3] }); + + expect(result.items).toEqual([]); + expect(result.total).toBe(0); + }); + + it("should handle tuple with null first element", () => { + const payload = [null, 5]; + + const result = normalizeListResponse(payload); + + // This should fall through to plain array handling + expect(result).toBeDefined(); + }); + }); + + describe("Type preservation", () => { + interface User { + id: number; + name: string; + } + + it("should preserve typed items", () => { + const payload = { + items: [ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, + ], + total: 2, + }; + + const result: ListResponse = normalizeListResponse(payload); + + expect(result.items[0].name).toBe("Alice"); + expect(result.items[1].id).toBe(2); + }); + }); +}); diff --git a/surfsense_web/tests/lib/utils.test.ts b/surfsense_web/tests/lib/utils.test.ts new file mode 100644 index 000000000..36bd806e5 --- /dev/null +++ b/surfsense_web/tests/lib/utils.test.ts @@ -0,0 +1,111 @@ +/** + * Tests for lib/utils.ts + * + * These tests validate: + * 1. cn() correctly merges Tailwind classes + * 2. getChatTitleFromMessages() extracts titles correctly + */ + +import { describe, it, expect } from "vitest"; +import { cn, getChatTitleFromMessages } from "@/lib/utils"; + +describe("cn - Class Name Merger", () => { + it("should merge simple class names", () => { + const result = cn("foo", "bar"); + expect(result).toBe("foo bar"); + }); + + it("should handle conditional classes", () => { + const isActive = true; + const result = cn("base", isActive && "active"); + expect(result).toBe("base active"); + }); + + it("should filter out falsy values", () => { + const result = cn("base", false, null, undefined, "valid"); + expect(result).toBe("base valid"); + }); + + it("should merge conflicting Tailwind classes (last wins)", () => { + // tailwind-merge should resolve conflicts + const result = cn("p-4", "p-2"); + expect(result).toBe("p-2"); + }); + + it("should handle object syntax", () => { + const result = cn({ + base: true, + active: true, + disabled: false, + }); + expect(result).toContain("base"); + expect(result).toContain("active"); + expect(result).not.toContain("disabled"); + }); + + it("should handle array syntax", () => { + const result = cn(["foo", "bar"]); + expect(result).toBe("foo bar"); + }); + + it("should handle empty input", () => { + const result = cn(); + expect(result).toBe(""); + }); + + it("should handle Tailwind responsive prefixes correctly", () => { + const result = cn("text-sm", "md:text-lg", "lg:text-xl"); + expect(result).toBe("text-sm md:text-lg lg:text-xl"); + }); + + it("should merge color classes properly", () => { + const result = cn("text-red-500", "text-blue-500"); + expect(result).toBe("text-blue-500"); + }); +}); + +describe("getChatTitleFromMessages", () => { + it("should return first user message content as title", () => { + const messages = [ + { id: "1", role: "user" as const, content: "Hello, how are you?" }, + { id: "2", role: "assistant" as const, content: "I am fine, thank you!" }, + ]; + + const title = getChatTitleFromMessages(messages); + expect(title).toBe("Hello, how are you?"); + }); + + it("should return 'Untitled Chat' when no user messages", () => { + const messages = [ + { id: "1", role: "assistant" as const, content: "Hello!" }, + { id: "2", role: "system" as const, content: "You are a helpful assistant" }, + ]; + + const title = getChatTitleFromMessages(messages); + expect(title).toBe("Untitled Chat"); + }); + + it("should return 'Untitled Chat' for empty messages array", () => { + const title = getChatTitleFromMessages([]); + expect(title).toBe("Untitled Chat"); + }); + + it("should use first user message even if there are multiple", () => { + const messages = [ + { id: "1", role: "assistant" as const, content: "Welcome!" }, + { id: "2", role: "user" as const, content: "First question" }, + { id: "3", role: "assistant" as const, content: "Answer" }, + { id: "4", role: "user" as const, content: "Second question" }, + ]; + + const title = getChatTitleFromMessages(messages); + expect(title).toBe("First question"); + }); + + it("should handle messages with only system role", () => { + const messages = [{ id: "1", role: "system" as const, content: "System prompt" }]; + + const title = getChatTitleFromMessages(messages); + expect(title).toBe("Untitled Chat"); + }); +});