diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx
index c8e748ada..4c455c63f 100644
--- a/apps/web/components/add-document/connections.tsx
+++ b/apps/web/components/add-document/connections.tsx
@@ -9,9 +9,11 @@ import { useCustomer } from "autumn-js/react"
import {
Check,
ChevronDown,
- Clock,
FolderOpen,
+ History,
Loader,
+ Loader2,
+ Play,
Trash2,
Zap,
} from "lucide-react"
@@ -30,6 +32,11 @@ import {
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
import { RemoveConnectionDialog } from "@/components/remove-connection-dialog"
+import { SyncStatusBadge } from "@/components/settings/sync-status-badge"
+import { SyncHistoryPanel } from "@/components/settings/sync-history-panel"
+import { useTriggerSync } from "@/hooks/use-trigger-sync"
+import { formatRelativeTime } from "@/components/settings/sync-utils"
+import type { ImportProvider } from "@/components/settings/sync-utils"
type GDriveSyncScope = "scoped" | "full"
@@ -71,17 +78,20 @@ const CONNECTORS: Record<
},
} as const
-function formatRelativeTime(date: string | null | undefined): string {
- if (!date) return "Never"
- const d = new Date(date)
- const diffMs = Date.now() - d.getTime()
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
- const diffDays = Math.floor(diffHours / 24)
- if (diffHours < 1) return "Just now"
- if (diffHours < 24) return `${diffHours}h ago`
- if (diffDays === 1) return "Yesterday"
- if (diffDays < 7) return `${diffDays} days ago`
- return d.toLocaleDateString()
+/** Extract typed metadata from a connection, with runtime validation. */
+function getConnectionMeta(connection: Connection) {
+ const m = connection.metadata as Record | undefined
+ return {
+ syncInProgress: m?.syncInProgress === true,
+ lastSyncedAt:
+ typeof m?.lastSyncedAt === "number" ? m.lastSyncedAt : undefined,
+ documentCount: typeof m?.documentCount === "number" ? m.documentCount : 0,
+ }
+}
+
+/** Check if a connection's auth token has expired. */
+function isConnectionExpired(connection: Connection): boolean {
+ return !!connection.expiresAt && new Date(connection.expiresAt) <= new Date()
}
function ConnectionRow({
@@ -89,18 +99,23 @@ function ConnectionRow({
onDelete,
isDeleting,
projects,
+ onTriggerSync,
+ isSyncing,
}: {
connection: Connection
onDelete: () => void
isDeleting: boolean
projects: Project[]
+ onTriggerSync: () => void
+ isSyncing: boolean
}) {
+ const [historyOpen, setHistoryOpen] = useState(false)
const config = CONNECTORS[connection.provider as ConnectorProvider]
if (!config) return null
const Icon = config.icon
- const isConnected =
- !connection.expiresAt || new Date(connection.expiresAt) > new Date()
+ const meta = getConnectionMeta(connection)
+ const expired = isConnectionExpired(connection)
const getProjectName = (tag: string): string => {
if (tag === DEFAULT_PROJECT_ID) return "Default"
@@ -110,12 +125,8 @@ function ConnectionRow({
)
}
- const documentCount = (connection.metadata?.documentCount as number) ?? 0
- const containerTags = (
- connection as Connection & { containerTags?: string[] }
- ).containerTags
- const projectName = containerTags?.[0]
- ? getProjectName(containerTags[0])
+ const projectName = connection.containerTags?.[0]
+ ? getProjectName(connection.containerTags[0])
: null
return (
@@ -138,23 +149,11 @@ function ConnectionRow({
>
{config.title}
-
-
-
- {isConnected ? "Connected" : "Disconnected"}
-
-
+
-
+
+
+
+
+
@@ -186,17 +242,11 @@ function ConnectionRow({
)}
-
-
-
- {formatRelativeTime(connection.createdAt)}
-
-
+
+ Last synced: {formatRelativeTime(meta.lastSyncedAt)}
+
- {documentCount}
+ {meta.documentCount}
+
+ {historyOpen && (
+
+
+
+ )}
)
@@ -236,6 +295,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
open: boolean
connection: Connection | null
}>({ open: false, connection: null })
+ const triggerSync = useTriggerSync()
const projects = (queryClient.getQueryData(["projects"]) ||
[]) as Project[]
@@ -282,7 +342,13 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
return response.data as Connection[]
},
staleTime: 30 * 1000,
- refetchInterval: 60 * 1000,
+ refetchInterval: (query) => {
+ const conns = query.state.data as Connection[] | undefined
+ if (conns?.some((c) => getConnectionMeta(c).syncInProgress)) {
+ return 5000
+ }
+ return 60 * 1000
+ },
refetchIntervalInBackground: true,
})
@@ -644,6 +710,18 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
projects={projects}
onDelete={() => setRemoveDialog({ open: true, connection })}
isDeleting={deleteConnectionMutation.isPending}
+ onTriggerSync={() =>
+ triggerSync.mutate({
+ connectionId: connection.id,
+ provider: connection.provider as ImportProvider,
+ containerTags: connection.containerTags,
+ })
+ }
+ isSyncing={
+ (triggerSync.isPending &&
+ triggerSync.variables?.connectionId === connection.id) ||
+ getConnectionMeta(connection).syncInProgress
+ }
/>
))}
diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx
index a7ae2d63a..676e251a8 100644
--- a/apps/web/components/settings/account.tsx
+++ b/apps/web/components/settings/account.tsx
@@ -197,6 +197,8 @@ export default function Account() {
} = useAuth()
const autumn = useCustomer()
const [isUpgrading, setIsUpgrading] = useState(false)
+ const [isCancelling, setIsCancelling] = useState(false)
+ const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
const [emailConfirm, setEmailConfirm] = useState("")
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const [isClosingAccount, setIsClosingAccount] = useState(false)
@@ -287,6 +289,33 @@ export default function Account() {
}
}
+ // Enterprise is contract-based — direct those users to the portal/sales.
+ const cancellablePlanId =
+ currentPlan === "pro" || currentPlan === "scale"
+ ? (`api_${currentPlan}` as const)
+ : null
+
+ const handleCancelSubscription = async () => {
+ if (!cancellablePlanId) return
+ setIsCancelling(true)
+ try {
+ await autumn.updateSubscription({
+ planId: cancellablePlanId,
+ cancelAction: "cancel_end_of_cycle",
+ })
+ autumn.refetch?.()
+ setIsCancelDialogOpen(false)
+ toast.success(
+ `Subscription cancelled. ${planDisplayNames[currentPlan]} features remain active until the end of your billing period.`,
+ )
+ } catch (error) {
+ console.error(error)
+ toast.error("Failed to cancel subscription. Please try again.")
+ } finally {
+ setIsCancelling(false)
+ }
+ }
+
const handleDeleteAccount = async () => {
if (!user?.email || !emailMatches || membershipsPending) return
setIsClosingAccount(true)
@@ -546,25 +575,141 @@ export default function Account() {
-