feat: Add notifications API routes and integrate with frontend

- Introduced new notifications API routes for marking notifications as read and marking all as read, with automatic syncing via Electric SQL.
- Updated the frontend hook to utilize the new API for marking notifications as read, enhancing the user experience with real-time updates.
- Included necessary response models for API interactions.
This commit is contained in:
Anish Sarkar 2026-01-14 02:59:58 +05:30
parent fede7413fb
commit 69f46ff3f4
3 changed files with 141 additions and 44 deletions

View file

@ -25,6 +25,7 @@ from .luma_add_connector_route import router as luma_add_connector_router
from .new_chat_routes import router as new_chat_router from .new_chat_routes import router as new_chat_router
from .new_llm_config_routes import router as new_llm_config_router from .new_llm_config_routes import router as new_llm_config_router
from .notes_routes import router as notes_router from .notes_routes import router as notes_router
from .notifications_routes import router as notifications_router
from .notion_add_connector_route import router as notion_add_connector_router from .notion_add_connector_route import router as notion_add_connector_router
from .podcasts_routes import router as podcasts_router from .podcasts_routes import router as podcasts_router
from .rbac_routes import router as rbac_router from .rbac_routes import router as rbac_router
@ -61,3 +62,4 @@ router.include_router(new_llm_config_router) # LLM configs with prompt configur
router.include_router(logs_router) router.include_router(logs_router)
router.include_router(circleback_webhook_router) # Circleback meeting webhooks router.include_router(circleback_webhook_router) # Circleback meeting webhooks
router.include_router(surfsense_docs_router) # Surfsense documentation for citations router.include_router(surfsense_docs_router) # Surfsense documentation for citations
router.include_router(notifications_router) # Notifications with Electric SQL sync

View file

@ -0,0 +1,102 @@
"""
Notifications API routes.
These endpoints allow marking notifications as read.
Electric SQL automatically syncs the changes to all connected clients.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import Notification, User, get_async_session
from app.users import current_active_user
router = APIRouter(prefix="/notifications", tags=["notifications"])
class MarkReadResponse(BaseModel):
"""Response for mark as read operations."""
success: bool
message: str
class MarkAllReadResponse(BaseModel):
"""Response for mark all as read operation."""
success: bool
message: str
updated_count: int
@router.patch("/{notification_id}/read", response_model=MarkReadResponse)
async def mark_notification_as_read(
notification_id: int,
user: User = Depends(current_active_user),
session: AsyncSession = Depends(get_async_session),
) -> MarkReadResponse:
"""
Mark a single notification as read.
Electric SQL will automatically sync this change to all connected clients.
"""
# Verify the notification belongs to the user
result = await session.execute(
select(Notification).where(
Notification.id == notification_id,
Notification.user_id == user.id,
)
)
notification = result.scalar_one_or_none()
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found",
)
if notification.read:
return MarkReadResponse(
success=True,
message="Notification already marked as read",
)
# Update the notification
notification.read = True
await session.commit()
return MarkReadResponse(
success=True,
message="Notification marked as read",
)
@router.patch("/read-all", response_model=MarkAllReadResponse)
async def mark_all_notifications_as_read(
user: User = Depends(current_active_user),
session: AsyncSession = Depends(get_async_session),
) -> MarkAllReadResponse:
"""
Mark all notifications as read for the current user.
Electric SQL will automatically sync these changes to all connected clients.
"""
# Update all unread notifications for the user
result = await session.execute(
update(Notification)
.where(
Notification.user_id == user.id,
Notification.read == False, # noqa: E712
)
.values(read=True)
)
await session.commit()
updated_count = result.rowcount
return MarkAllReadResponse(
success=True,
message=f"Marked {updated_count} notification(s) as read",
updated_count=updated_count,
)

View file

@ -1,13 +1,9 @@
"use client"; "use client";
import { useEffect, useState, useCallback, useRef } from "react"; import { useEffect, useState, useCallback, useRef } from "react";
import { import { initElectric, type ElectricClient, type SyncHandle } from "@/lib/electric/client";
initElectric,
isElectricInitialized,
type ElectricClient,
type SyncHandle,
} from "@/lib/electric/client";
import type { Notification } from "@/contracts/types/notification.types"; import type { Notification } from "@/contracts/types/notification.types";
import { authenticatedFetch } from "@/lib/auth-utils";
export type { Notification } from "@/contracts/types/notification.types"; export type { Notification } from "@/contracts/types/notification.types";
@ -215,56 +211,53 @@ export function useNotifications(userId: string | null) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]); }, [userId]);
// Mark notification as read (local only - needs backend sync) // Mark notification as read via backend API
const markAsRead = useCallback( // Electric SQL will automatically sync the change to all clients
async (notificationId: number) => { const markAsRead = useCallback(async (notificationId: number) => {
if (!electric || !isElectricInitialized()) { try {
console.warn("Electric SQL not initialized"); // Call backend API - Electric SQL will sync the change automatically
return false; const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/notifications/${notificationId}/read`,
{ method: "PATCH" }
);
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: "Failed to mark as read" }));
throw new Error(error.detail || "Failed to mark notification as read");
} }
try { // Electric SQL will sync the change from PostgreSQL to PGlite automatically
// Update locally in PGlite // The live query subscription will update the UI
await electric.db.query( return true;
`UPDATE notifications SET read = true, updated_at = NOW() WHERE id = $1`, } catch (err) {
[notificationId] console.error("Failed to mark notification as read:", err);
);
// Update local state
setNotifications((prev) =>
prev.map((n) => (n.id === notificationId ? { ...n, read: true } : n))
);
// TODO: Also send to backend to persist the change
// This could be done via a REST API call
return true;
} catch (err) {
console.error("Failed to mark notification as read:", err);
return false;
}
},
[electric]
);
// Mark all notifications as read
const markAllAsRead = useCallback(async () => {
if (!electric || !isElectricInitialized()) {
console.warn("Electric SQL not initialized");
return false; return false;
} }
}, []);
// Mark all notifications as read via backend API
// Electric SQL will automatically sync the changes to all clients
const markAllAsRead = useCallback(async () => {
try { try {
const unread = notifications.filter((n) => !n.read); // Call backend API - Electric SQL will sync all changes automatically
for (const notification of unread) { const response = await authenticatedFetch(
await markAsRead(notification.id); `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/notifications/read-all`,
{ method: "PATCH" }
);
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: "Failed to mark all as read" }));
throw new Error(error.detail || "Failed to mark all notifications as read");
} }
// Electric SQL will sync the changes from PostgreSQL to PGlite automatically
// The live query subscription will update the UI
return true; return true;
} catch (err) { } catch (err) {
console.error("Failed to mark all notifications as read:", err); console.error("Failed to mark all notifications as read:", err);
return false; return false;
} }
}, [electric, notifications, markAsRead]); }, []);
// Get unread count // Get unread count
const unreadCount = notifications.filter((n) => !n.read).length; const unreadCount = notifications.filter((n) => !n.read).length;