diff --git a/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py b/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py
index e07cf6de4..dc25a1edd 100644
--- a/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py
+++ b/surfsense_backend/alembic/versions/66_add_notifications_table_and_electric_replication.py
@@ -19,7 +19,7 @@ from alembic import context, op
# Get Electric SQL user credentials from env.py configuration
_config = context.config
-ELECTRIC_DB_USER =_config.get_main_option("electric_db_user", "electric")
+ELECTRIC_DB_USER = _config.get_main_option("electric_db_user", "electric")
ELECTRIC_DB_PASSWORD = _config.get_main_option(
"electric_db_password", "electric_password"
)
@@ -52,10 +52,18 @@ def upgrade() -> None:
)
# Create indexes (using IF NOT EXISTS for idempotency)
- op.execute("CREATE INDEX IF NOT EXISTS ix_notifications_user_id ON notifications (user_id);")
- op.execute("CREATE INDEX IF NOT EXISTS ix_notifications_read ON notifications (read);")
- op.execute("CREATE INDEX IF NOT EXISTS ix_notifications_created_at ON notifications (created_at);")
- op.execute("CREATE INDEX IF NOT EXISTS ix_notifications_user_read ON notifications (user_id, read);")
+ op.execute(
+ "CREATE INDEX IF NOT EXISTS ix_notifications_user_id ON notifications (user_id);"
+ )
+ op.execute(
+ "CREATE INDEX IF NOT EXISTS ix_notifications_read ON notifications (read);"
+ )
+ op.execute(
+ "CREATE INDEX IF NOT EXISTS ix_notifications_created_at ON notifications (created_at);"
+ )
+ op.execute(
+ "CREATE INDEX IF NOT EXISTS ix_notifications_user_read ON notifications (user_id, read);"
+ )
# =====================================================
# Electric SQL Setup - User and Publication
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 049183cf3..2320b3b9a 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
@@ -438,9 +438,7 @@ export default function EditorPage() {
{saving ? (
<>
-
- {isNewNote ? "Creating" : "Saving"}
-
+ {isNewNote ? "Creating" : "Saving"}
>
) : (
<>
diff --git a/surfsense_web/components/notifications/NotificationButton.tsx b/surfsense_web/components/notifications/NotificationButton.tsx
index b1edd2254..26b4713b5 100644
--- a/surfsense_web/components/notifications/NotificationButton.tsx
+++ b/surfsense_web/components/notifications/NotificationButton.tsx
@@ -14,15 +14,15 @@ import { useParams } from "next/navigation";
export function NotificationButton() {
const { data: user } = useAtomValue(currentUserAtom);
const params = useParams();
-
+
const userId = user?.id ? String(user.id) : null;
// Get searchSpaceId from URL params - the component is rendered within /dashboard/[search_space_id]/
- const searchSpaceId = params?.search_space_id
- ? Number(params.search_space_id)
- : null;
-
- const { notifications, unreadCount, loading, markAsRead, markAllAsRead } =
- useNotifications(userId, searchSpaceId);
+ const searchSpaceId = params?.search_space_id ? Number(params.search_space_id) : null;
+
+ const { notifications, unreadCount, loading, markAsRead, markAllAsRead } = useNotifications(
+ userId,
+ searchSpaceId
+ );
return (
diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx
index 9a296f314..af3046a64 100644
--- a/surfsense_web/components/providers/ElectricProvider.tsx
+++ b/surfsense_web/components/providers/ElectricProvider.tsx
@@ -29,7 +29,11 @@ interface ElectricProviderProps {
export function ElectricProvider({ children }: ElectricProviderProps) {
const [electricClient, setElectricClient] = useState(null);
const [error, setError] = useState(null);
- const { data: user, isSuccess: isUserLoaded, isError: isUserError } = useAtomValue(currentUserAtom);
+ const {
+ data: user,
+ isSuccess: isUserLoaded,
+ isError: isUserError,
+ } = useAtomValue(currentUserAtom);
const previousUserIdRef = useRef(null);
const initializingRef = useRef(false);
@@ -104,11 +108,7 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
// For non-authenticated pages (like landing page), render immediately with null context
// Also render immediately if user query failed (e.g., token expired)
if (!isUserLoaded || !user?.id || isUserError) {
- return (
-
- {children}
-
- );
+ return {children};
}
// Show loading state while initializing for authenticated users
@@ -128,9 +128,5 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
}
// Provide the Electric client to children
- return (
-
- {children}
-
- );
+ return {children};
}
diff --git a/surfsense_web/hooks/use-connectors-electric.ts b/surfsense_web/hooks/use-connectors-electric.ts
index 77c18f0a5..ccda052b1 100644
--- a/surfsense_web/hooks/use-connectors-electric.ts
+++ b/surfsense_web/hooks/use-connectors-electric.ts
@@ -7,14 +7,14 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types";
/**
* Hook for managing connectors with Electric SQL real-time sync
- *
+ *
* Uses the Electric client from context (provided by ElectricProvider)
* instead of initializing its own - prevents race conditions and memory leaks
*/
export function useConnectorsElectric(searchSpaceId: number | string | null) {
// Get Electric client from context - ElectricProvider handles initialization
const electricClient = useElectricClient();
-
+
const [connectors, setConnectors] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -173,7 +173,7 @@ export function useConnectorsElectric(searchSpaceId: number | string | null) {
return () => {
mounted = false;
syncKeyRef.current = null;
-
+
if (syncHandleRef.current) {
syncHandleRef.current.unsubscribe();
syncHandleRef.current = null;
diff --git a/surfsense_web/hooks/use-documents-electric.ts b/surfsense_web/hooks/use-documents-electric.ts
index 6e50c775c..74d9e91e7 100644
--- a/surfsense_web/hooks/use-documents-electric.ts
+++ b/surfsense_web/hooks/use-documents-electric.ts
@@ -13,14 +13,14 @@ interface Document {
/**
* Hook for managing documents with Electric SQL real-time sync
- *
+ *
* Uses the Electric client from context (provided by ElectricProvider)
* instead of initializing its own - prevents race conditions and memory leaks
*/
export function useDocumentsElectric(searchSpaceId: number | string | null) {
// Get Electric client from context - ElectricProvider handles initialization
const electricClient = useElectricClient();
-
+
const [documents, setDocuments] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -169,7 +169,7 @@ export function useDocumentsElectric(searchSpaceId: number | string | null) {
return () => {
mounted = false;
syncKeyRef.current = null;
-
+
if (syncHandleRef.current) {
syncHandleRef.current.unsubscribe();
syncHandleRef.current = null;
diff --git a/surfsense_web/hooks/use-notifications.ts b/surfsense_web/hooks/use-notifications.ts
index 62617ede2..7a3b49861 100644
--- a/surfsense_web/hooks/use-notifications.ts
+++ b/surfsense_web/hooks/use-notifications.ts
@@ -10,29 +10,29 @@ export type { Notification } from "@/contracts/types/notification.types";
/**
* Hook for managing notifications with Electric SQL real-time sync
- *
+ *
* Uses the Electric client from context (provided by ElectricProvider)
* instead of initializing its own - prevents race conditions and memory leaks
- *
+ *
* Architecture:
* - User-level sync: Syncs ALL notifications for a user (runs once per user)
* - Search-space-level query: Filters notifications by searchSpaceId (updates on search space change)
- *
+ *
* This separation ensures smooth transitions when switching search spaces (no flash).
- *
+ *
* @param userId - The user ID to fetch notifications for
* @param searchSpaceId - The search space ID to filter notifications (null shows global notifications only)
*/
export function useNotifications(userId: string | null, searchSpaceId: number | null) {
// Get Electric client from context - ElectricProvider handles initialization
const electricClient = useElectricClient();
-
+
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const syncHandleRef = useRef(null);
const liveQueryRef = useRef<{ unsubscribe: () => void } | null>(null);
-
+
// Track user-level sync key to prevent duplicate sync subscriptions
const userSyncKeyRef = useRef(null);
@@ -100,7 +100,7 @@ export function useNotifications(userId: string | null, searchSpaceId: number |
return () => {
mounted = false;
userSyncKeyRef.current = null;
-
+
if (syncHandleRef.current) {
syncHandleRef.current.unsubscribe();
syncHandleRef.current = null;
@@ -135,7 +135,7 @@ export function useNotifications(userId: string | null, searchSpaceId: number |
ORDER BY created_at DESC`,
[userId, searchSpaceId]
);
-
+
if (mounted) {
setNotifications(result.rows || []);
}
diff --git a/surfsense_web/lib/electric/client.ts b/surfsense_web/lib/electric/client.ts
index 801b7e963..473e06461 100644
--- a/surfsense_web/lib/electric/client.ts
+++ b/surfsense_web/lib/electric/client.ts
@@ -381,7 +381,10 @@ export async function initElectric(userId: string): Promise {
},
};
- console.log("[Electric] syncShapeToTable config:", JSON.stringify(shapeConfig, null, 2));
+ console.log(
+ "[Electric] syncShapeToTable config:",
+ JSON.stringify(shapeConfig, null, 2)
+ );
// Type assertion to PGlite with electric extension
const pgWithElectric = db as PGlite & {
@@ -452,9 +455,7 @@ export async function initElectric(userId: string): Promise {
(typeof msg === "object" && "up-to-date" in msg)
) {
if (!syncResolved) {
- console.log(
- `[Electric] ✅ Received up-to-date message for ${table}`
- );
+ console.log(`[Electric] ✅ Received up-to-date message for ${table}`);
resolveInitialSync();
}
// Continue listening for real-time updates - don't return!
@@ -477,9 +478,7 @@ export async function initElectric(userId: string): Promise {
// Also check stream's isUpToDate property immediately
if (stream?.isUpToDate) {
- console.log(
- `[Electric] ✅ Stream isUpToDate is true immediately for ${table}`
- );
+ console.log(`[Electric] ✅ Stream isUpToDate is true immediately for ${table}`);
resolveInitialSync();
}
}
@@ -530,7 +529,9 @@ export async function initElectric(userId: string): Promise {
// Cache the sync handle for reuse (memory optimization)
activeSyncHandles.set(cacheKey, syncHandle);
- console.log(`[Electric] Cached sync handle for: ${cacheKey} (total cached: ${activeSyncHandles.size})`);
+ console.log(
+ `[Electric] Cached sync handle for: ${cacheKey} (total cached: ${activeSyncHandles.size})`
+ );
return syncHandle;
} catch (error) {
diff --git a/surfsense_web/lib/electric/context.ts b/surfsense_web/lib/electric/context.ts
index 010959ed2..777d4e12c 100644
--- a/surfsense_web/lib/electric/context.ts
+++ b/surfsense_web/lib/electric/context.ts
@@ -5,7 +5,7 @@ import type { ElectricClient } from "./client";
/**
* Context for sharing the Electric SQL client across the app
- *
+ *
* This ensures:
* 1. Single initialization point (ElectricProvider only)
* 2. No race conditions (hooks wait for context)
@@ -34,4 +34,3 @@ export function useElectricClientOrThrow(): ElectricClient {
}
return client;
}
-