diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts
index e82a8eb29..289da475d 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts
+++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts
@@ -10,8 +10,9 @@ import { isConnectorIndexingMetadata } from "@/contracts/types/inbox.types";
*
* This provides a better UX than polling by:
* 1. Setting indexing state immediately when user triggers indexing (optimistic)
- * 2. Clearing indexing state when Electric SQL detects last_indexed_at changed
- * 3. Clearing indexing state when a failed notification is detected
+ * 2. Detecting in_progress notifications from Electric SQL to restore state after remounts
+ * 3. Clearing indexing state when notifications become completed or failed
+ * 4. Clearing indexing state when Electric SQL detects last_indexed_at changed
*
* The actual `last_indexed_at` value comes from Electric SQL/PGlite, not local state.
*/
@@ -28,65 +29,73 @@ export function useIndexingConnectors(
// Detect when last_indexed_at changes (indexing completed) via Electric SQL
useEffect(() => {
const previousValues = previousLastIndexedAtRef.current;
- const newIndexingIds = new Set(indexingConnectorIds);
- let hasChanges = false;
for (const connector of connectors) {
const previousValue = previousValues.get(connector.id);
const currentValue = connector.last_indexed_at;
- // If last_indexed_at changed and connector was in indexing state, clear it
+ // If last_indexed_at changed, clear it from indexing state
if (
previousValue !== undefined && // We've seen this connector before
- previousValue !== currentValue && // Value changed
- indexingConnectorIds.has(connector.id) // It was marked as indexing
+ previousValue !== currentValue // Value changed
) {
- newIndexingIds.delete(connector.id);
- hasChanges = true;
+ // Use functional update to access current state
+ setIndexingConnectorIds((prev) => {
+ if (prev.has(connector.id)) {
+ const next = new Set(prev);
+ next.delete(connector.id);
+ return next;
+ }
+ return prev;
+ });
}
// Update previous value tracking
previousValues.set(connector.id, currentValue);
}
+ }, [connectors]);
- if (hasChanges) {
- setIndexingConnectorIds(newIndexingIds);
- }
- }, [connectors, indexingConnectorIds]);
-
- // Detect failed notifications and stop indexing state
+ // Detect notification status changes and update indexing state accordingly
+ // This restores spinner state after component remounts and handles all status transitions
useEffect(() => {
if (!inboxItems || inboxItems.length === 0) return;
- const newIndexingIds = new Set(indexingConnectorIds);
- let hasChanges = false;
+ setIndexingConnectorIds((prev) => {
+ const newIndexingIds = new Set(prev);
+ let hasChanges = false;
- for (const item of inboxItems) {
- // Only check connector_indexing notifications
- if (item.type !== "connector_indexing") continue;
+ for (const item of inboxItems) {
+ // Only check connector_indexing notifications
+ if (item.type !== "connector_indexing") continue;
- // Check if this notification indicates a failure
- const metadata = isConnectorIndexingMetadata(item.metadata)
- ? item.metadata
- : null;
- if (!metadata) continue;
+ const metadata = isConnectorIndexingMetadata(item.metadata)
+ ? item.metadata
+ : null;
+ if (!metadata) continue;
- // Check if status is "failed" or if there's an error_message
- const isFailed =
- metadata.status === "failed" ||
- (metadata.error_message && metadata.error_message.trim().length > 0);
-
- // If failed and connector is in indexing state, clear it
- if (isFailed && indexingConnectorIds.has(metadata.connector_id)) {
- newIndexingIds.delete(metadata.connector_id);
- hasChanges = true;
+ // If status is "in_progress", add connector to indexing set
+ if (metadata.status === "in_progress") {
+ if (!newIndexingIds.has(metadata.connector_id)) {
+ newIndexingIds.add(metadata.connector_id);
+ hasChanges = true;
+ }
+ }
+ // If status is "completed" or "failed", remove connector from indexing set
+ else if (
+ metadata.status === "completed" ||
+ metadata.status === "failed" ||
+ (metadata.error_message && metadata.error_message.trim().length > 0)
+ ) {
+ if (newIndexingIds.has(metadata.connector_id)) {
+ newIndexingIds.delete(metadata.connector_id);
+ hasChanges = true;
+ }
+ }
}
- }
- if (hasChanges) {
- setIndexingConnectorIds(newIndexingIds);
- }
- }, [inboxItems, indexingConnectorIds]);
+ return hasChanges ? newIndexingIds : prev;
+ });
+ }, [inboxItems]);
// Add a connector to the indexing set (called when indexing starts)
const startIndexing = useCallback((connectorId: number) => {
diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
index 52dc7196a..9e3f55c97 100644
--- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
@@ -38,6 +38,17 @@ interface LayoutDataProviderProps {
breadcrumb?: React.ReactNode;
}
+/**
+ * Format count for display: shows numbers up to 999, then "1k+", "2k+", etc.
+ */
+function formatInboxCount(count: number): string {
+ if (count <= 999) {
+ return count.toString();
+ }
+ const thousands = Math.floor(count / 1000);
+ return `${thousands}k+`;
+}
+
export function LayoutDataProvider({
searchSpaceId,
children,
@@ -172,7 +183,7 @@ export function LayoutDataProvider({
url: "#inbox", // Special URL to indicate this is handled differently
icon: Inbox,
isActive: isInboxSidebarOpen,
- badge: unreadCount > 0 ? (unreadCount > 99 ? "99+" : unreadCount) : undefined,
+ badge: unreadCount > 0 ? formatInboxCount(unreadCount) : undefined,
},
],
[searchSpaceId, pathname, isInboxSidebarOpen, unreadCount]
diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx
index bb06d6a56..e80c6e62d 100644
--- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx
@@ -70,6 +70,17 @@ function getInitials(name: string | null | undefined, email: string | null | und
return "U";
}
+/**
+ * Format count for display: shows numbers up to 999, then "1k+", "2k+", etc.
+ */
+function formatInboxCount(count: number): string {
+ if (count <= 999) {
+ return count.toString();
+ }
+ const thousands = Math.floor(count / 1000);
+ return `${thousands}k+`;
+}
+
/**
* Get display name for connector type
*/
@@ -732,7 +743,7 @@ export function InboxSidebar({
{t("mentions") || "Mentions"}
- {unreadMentionsCount}
+ {formatInboxCount(unreadMentionsCount)}
@@ -744,7 +755,7 @@ export function InboxSidebar({
{t("status") || "Status"}
- {unreadStatusCount}
+ {formatInboxCount(unreadStatusCount)}
diff --git a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx
index d2d926de8..742a27bbc 100644
--- a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx
@@ -39,7 +39,7 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
>
{item.badge && (
-
+
{item.badge}
)}
@@ -70,7 +70,7 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
{item.title}
{item.badge && (
-
+
{item.badge}
)}