diff --git a/surfsense_backend/alembic/versions/62_add_notifications_table.py b/surfsense_backend/alembic/versions/62_add_notifications_table.py index 5af37d2f8..cf3407415 100644 --- a/surfsense_backend/alembic/versions/62_add_notifications_table.py +++ b/surfsense_backend/alembic/versions/62_add_notifications_table.py @@ -1,10 +1,11 @@ -"""Add notifications table +"""Add notifications table and Electric SQL replication Revision ID: 62 Revises: 61 -Note: Electric SQL replication setup (REPLICA IDENTITY FULL and publication) -is handled in app/db.py setup_electric_replication() which runs on app startup. +Creates notifications table and sets up Electric SQL replication +(REPLICA IDENTITY FULL and publication) for notifications, +search_source_connectors, and documents tables. """ from collections.abc import Sequence @@ -19,7 +20,7 @@ depends_on: str | Sequence[str] | None = None def upgrade() -> None: - """Upgrade schema - add notifications table.""" + """Upgrade schema - add notifications table and Electric SQL replication.""" # Create notifications table op.execute( """ @@ -44,6 +45,51 @@ def upgrade() -> None: op.create_index("ix_notifications_created_at", "notifications", ["created_at"]) op.create_index("ix_notifications_user_read", "notifications", ["user_id", "read"]) + # Set up Electric SQL replication for real-time sync tables + # Set REPLICA IDENTITY FULL (required by Electric SQL for replication) + # This logs full row data for UPDATE/DELETE operations in the WAL + op.execute("ALTER TABLE notifications REPLICA IDENTITY FULL;") + op.execute("ALTER TABLE search_source_connectors REPLICA IDENTITY FULL;") + op.execute("ALTER TABLE documents REPLICA IDENTITY FULL;") + + # Add tables to Electric SQL publication for replication if publication exists + op.execute( + """ + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'electric_publication_default') THEN + -- Add notifications if not already added + IF NOT EXISTS ( + SELECT 1 FROM pg_publication_tables + WHERE pubname = 'electric_publication_default' + AND tablename = 'notifications' + ) THEN + ALTER PUBLICATION electric_publication_default ADD TABLE notifications; + END IF; + + -- Add search_source_connectors if not already added + IF NOT EXISTS ( + SELECT 1 FROM pg_publication_tables + WHERE pubname = 'electric_publication_default' + AND tablename = 'search_source_connectors' + ) THEN + ALTER PUBLICATION electric_publication_default ADD TABLE search_source_connectors; + END IF; + + -- Add documents if not already added + IF NOT EXISTS ( + SELECT 1 FROM pg_publication_tables + WHERE pubname = 'electric_publication_default' + AND tablename = 'documents' + ) THEN + ALTER PUBLICATION electric_publication_default ADD TABLE documents; + END IF; + END IF; + END + $$; + """ + ) + def downgrade() -> None: """Downgrade schema - remove notifications table.""" diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 7af55eaa3..699da9c38 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -988,60 +988,6 @@ async def create_db_and_tables(): await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) await conn.run_sync(Base.metadata.create_all) await setup_indexes() - await setup_electric_replication() - - -async def setup_electric_replication(): - """Set up Electric SQL replication for real-time sync tables.""" - async with engine.begin() as conn: - # Set REPLICA IDENTITY FULL (required by Electric SQL for replication) - # This logs full row data for UPDATE/DELETE operations in the WAL - await conn.execute(text("ALTER TABLE notifications REPLICA IDENTITY FULL;")) - await conn.execute( - text("ALTER TABLE search_source_connectors REPLICA IDENTITY FULL;") - ) - await conn.execute(text("ALTER TABLE documents REPLICA IDENTITY FULL;")) - - # Add tables to Electric SQL publication for replication - # Only add if publication exists and table not already in it - await conn.execute( - text( - """ - DO $$ - BEGIN - IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'electric_publication_default') THEN - -- Add notifications if not already added - IF NOT EXISTS ( - SELECT 1 FROM pg_publication_tables - WHERE pubname = 'electric_publication_default' - AND tablename = 'notifications' - ) THEN - ALTER PUBLICATION electric_publication_default ADD TABLE notifications; - END IF; - - -- Add search_source_connectors if not already added - IF NOT EXISTS ( - SELECT 1 FROM pg_publication_tables - WHERE pubname = 'electric_publication_default' - AND tablename = 'search_source_connectors' - ) THEN - ALTER PUBLICATION electric_publication_default ADD TABLE search_source_connectors; - END IF; - - -- Add documents if not already added - IF NOT EXISTS ( - SELECT 1 FROM pg_publication_tables - WHERE pubname = 'electric_publication_default' - AND tablename = 'documents' - ) THEN - ALTER PUBLICATION electric_publication_default ADD TABLE documents; - END IF; - END IF; - END - $$; - """ - ) - ) async def get_async_session() -> AsyncGenerator[AsyncSession, None]: 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 59593ed8e..01891f05b 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 @@ -148,7 +148,7 @@ export default function DocumentsTable() { document_type: "SURFSENSE_DOCS", document_metadata: { source: doc.source }, content: doc.content, - created_at: doc.created_at || doc.updated_at || new Date().toISOString(), + created_at: new Date().toISOString(), search_space_id: -1, // Special value for global docs })); }, [surfsenseDocsResponse]); diff --git a/surfsense_web/components/assistant-ui/document-upload-popup.tsx b/surfsense_web/components/assistant-ui/document-upload-popup.tsx index 29f633ebf..58fcc12a0 100644 --- a/surfsense_web/components/assistant-ui/document-upload-popup.tsx +++ b/surfsense_web/components/assistant-ui/document-upload-popup.tsx @@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"; import { Upload } from "lucide-react"; -import { useRouter } from "next/navigation"; import { createContext, type FC, @@ -85,13 +84,11 @@ const DocumentUploadPopupContent: FC<{ onOpenChange: (open: boolean) => void; }> = ({ isOpen, onOpenChange }) => { const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); - const router = useRouter(); if (!searchSpaceId) return null; const handleSuccess = () => { onOpenChange(false); - router.push(`/dashboard/${searchSpaceId}/documents`); }; return ( diff --git a/surfsense_web/components/notifications/NotificationPopup.tsx b/surfsense_web/components/notifications/NotificationPopup.tsx index f194aa394..3355b62e6 100644 --- a/surfsense_web/components/notifications/NotificationPopup.tsx +++ b/surfsense_web/components/notifications/NotificationPopup.tsx @@ -1,6 +1,6 @@ "use client"; -import { Bell, Check, CheckCheck, Loader2, AlertCircle, CheckCircle2 } from "lucide-react"; +import { Bell, CheckCheck, Loader2, AlertCircle, CheckCircle2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; @@ -44,7 +44,7 @@ export function NotificationPopup({ switch (status) { case "in_progress": - return ; + return ; case "completed": return ; case "failed": diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 0b7f7b51f..a261fc949 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -3,7 +3,6 @@ import { useAtom } from "jotai"; import { CheckCircle2, FileType, Info, Loader2, Tag, Upload, X } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; -import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useMemo, useRef, useState } from "react"; import { useDropzone } from "react-dropzone"; @@ -116,7 +115,6 @@ export function DocumentUploadTab({ onAccordionStateChange, }: DocumentUploadTabProps) { const t = useTranslations("upload_documents"); - const router = useRouter(); const [files, setFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState(0); const [accordionValue, setAccordionValue] = useState(""); @@ -185,7 +183,7 @@ export function DocumentUploadTab({ setUploadProgress(100); trackDocumentUploadSuccess(Number(searchSpaceId), files.length); toast(t("upload_initiated"), { description: t("upload_initiated_desc") }); - onSuccess?.() || router.push(`/dashboard/${searchSpaceId}/documents`); + onSuccess?.(); }, onError: (error: unknown) => { clearInterval(progressInterval);