refactor: address review feedback for sync visibility

- Restore 60s baseline polling (was regressed to false)
- Extract getConnectionMeta helper to eliminate 5 inline casts
- Remove unnecessary containerTags intersection casts
- Deduplicate provider title maps into shared PROVIDER_DISPLAY_NAMES
- Fix formatRelativeTime: handle future dates, add minutes tier
- Preserve auth/expired state in SyncStatusBadge (red Disconnected)
- Disable sync button for expired connections
- Fix mobile width overflow on sync history sheet
- Add smart polling to useSyncRuns when a run is running
- Add clarifying comment on connectionId in useTriggerSync
- Replace z.any() with z.unknown() for import endpoint output
- Remove unnecessary 'use client' from sync-status-badge
This commit is contained in:
MaheshtheDev 2026-05-11 20:32:56 +00:00
parent eeb728e000
commit 1c1901f711
7 changed files with 102 additions and 56 deletions

View file

@ -28,6 +28,22 @@ import type { ImportProvider } from "@/components/settings/sync-utils"
type Connection = z.infer<typeof ConnectionResponseSchema>
/** Extract typed metadata from a connection, with runtime validation. */
function getConnectionMeta(connection: Connection) {
const m = connection.metadata as Record<string, unknown> | 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()
}
const CONNECTORS = {
"google-drive": {
title: "Google Drive",
@ -156,6 +172,8 @@ function ConnectionRow({
if (!config) return null
const Icon = config.icon
const meta = getConnectionMeta(connection)
const expired = isConnectionExpired(connection)
const getProjectDisplayName = (containerTag: string): string => {
if (containerTag === DEFAULT_PROJECT_ID) return "Default Project"
@ -164,13 +182,11 @@ function ConnectionRow({
return containerTag.replace(/^sm_project_/, "") // if cached project is not found, remove the prefix
}
const documentCount = (connection.metadata?.documentCount as number) ?? 0
const containerTags = (
connection as Connection & { containerTags?: string[] }
).containerTags
const projectName =
containerTags && containerTags.length > 0 && containerTags[0]
? getProjectDisplayName(containerTags[0])
connection.containerTags &&
connection.containerTags.length > 0 &&
connection.containerTags[0]
? getProjectDisplayName(connection.containerTags[0])
: null
return (
@ -195,14 +211,9 @@ function ConnectionRow({
{config.title}
</span>
<SyncStatusBadge
syncInProgress={
(connection.metadata as Record<string, unknown>)
?.syncInProgress as boolean | undefined
}
lastSyncedAt={
(connection.metadata as Record<string, unknown>)
?.lastSyncedAt as number | undefined
}
syncInProgress={meta.syncInProgress}
lastSyncedAt={meta.lastSyncedAt}
isExpired={expired}
/>
</div>
<span
@ -221,10 +232,22 @@ function ConnectionRow({
e.stopPropagation()
onTriggerSync()
}}
disabled={isSyncing || disabled}
disabled={isSyncing || disabled || expired}
className="text-[#737373] hover:text-[#4BA0FA] transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5"
aria-label={isSyncing ? "Sync in progress" : "Sync now"}
title={isSyncing ? "Sync in progress" : "Sync now"}
aria-label={
expired
? "Connection expired"
: isSyncing
? "Sync in progress"
: "Sync now"
}
title={
expired
? "Reconnect to sync"
: isSyncing
? "Sync in progress"
: "Sync now"
}
>
{isSyncing ? (
<Loader2 className="size-[18px] animate-spin" />
@ -279,12 +302,7 @@ function ConnectionRow({
"font-medium text-[14px] tracking-[-0.14px] text-[#737373]",
)}
>
Last synced:{" "}
{formatRelativeTime(
(connection.metadata as Record<string, unknown>)?.lastSyncedAt as
| number
| undefined,
)}
Last synced: {formatRelativeTime(meta.lastSyncedAt)}
</span>
<div className="size-[3px] rounded-full bg-[#737373]" />
<span
@ -293,7 +311,7 @@ function ConnectionRow({
"font-medium text-[14px] tracking-[-0.14px] text-[#737373]",
)}
>
{documentCount} {config.documentLabel} connected
{meta.documentCount} {config.documentLabel} connected
</span>
</div>
</div>
@ -399,14 +417,10 @@ export default function ConnectionsMCP() {
staleTime: 30 * 1000,
refetchInterval: (query) => {
const conns = query.state.data as Connection[] | undefined
if (
conns?.some(
(c) => (c.metadata as Record<string, unknown>)?.syncInProgress,
)
) {
if (conns?.some((c) => getConnectionMeta(c).syncInProgress)) {
return 5000
}
return false
return 60 * 1000
},
enabled: hasProProduct,
})
@ -532,19 +546,14 @@ export default function ConnectionsMCP() {
triggerSync.mutate({
connectionId: connection.id,
provider: connection.provider as ImportProvider,
containerTags: (
connection as Connection & {
containerTags?: string[]
}
).containerTags,
containerTags: connection.containerTags,
})
}
isSyncing={
(triggerSync.isPending &&
triggerSync.variables?.connectionId ===
connection.id) ||
!!(connection.metadata as Record<string, unknown>)
?.syncInProgress
getConnectionMeta(connection).syncInProgress
}
onViewHistory={() =>
setSyncHistorySheet({ open: true, connection })
@ -635,7 +644,9 @@ export default function ConnectionsMCP() {
}}
provider={removeDialog.connection?.provider}
documentCount={
(removeDialog.connection?.metadata?.documentCount as number) ?? 0
removeDialog.connection
? getConnectionMeta(removeDialog.connection).documentCount
: 0
}
onConfirm={(deleteDocuments) => {
if (removeDialog.connection) {

View file

@ -14,22 +14,13 @@ import type { SyncRun } from "@/hooks/use-sync-runs"
import {
formatRelativeTime,
TRIGGER_TYPE_LABELS,
PROVIDER_DISPLAY_NAMES,
} from "@/components/settings/sync-utils"
import type { ConnectionResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
type Connection = z.infer<typeof ConnectionResponseSchema>
const PROVIDER_TITLES: Record<string, string> = {
"google-drive": "Google Drive",
notion: "Notion",
onedrive: "OneDrive",
gmail: "Gmail",
github: "GitHub",
"web-crawler": "Web Crawler",
s3: "S3",
}
const STATUS_COLORS: Record<string, { dot: string; text: string }> = {
completed: { dot: "bg-[#00AC3F]", text: "text-[#00AC3F]" },
failed: { dot: "bg-[#EF4444]", text: "text-[#EF4444]" },
@ -122,7 +113,7 @@ export function SyncHistorySheet({
} = useSyncRuns(open && connection ? connection.id : "")
const providerTitle = connection
? (PROVIDER_TITLES[connection.provider] ?? connection.provider)
? (PROVIDER_DISPLAY_NAMES[connection.provider] ?? connection.provider)
: ""
const email = connection?.email ?? ""
@ -130,7 +121,7 @@ export function SyncHistorySheet({
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
className="bg-[#0D0F14] border-l border-[#1A1D24] w-[400px] sm:max-w-[400px] gap-0 overflow-hidden p-0 [&>button]:text-[#737373] [&>button]:hover:text-[#FAFAFA]"
className="bg-[#0D0F14] border-l border-[#1A1D24] w-full max-w-[400px] gap-0 overflow-hidden p-0 [&>button]:text-[#737373] [&>button]:hover:text-[#FAFAFA]"
>
<SheetHeader className="px-6 pt-6 pb-4 border-b border-[#1A1D24]">
<SheetTitle

View file

@ -1,5 +1,3 @@
"use client"
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { formatRelativeTime } from "@/components/settings/sync-utils"
@ -7,7 +5,9 @@ import { formatRelativeTime } from "@/components/settings/sync-utils"
function deriveStatus(
syncInProgress?: boolean,
lastSyncedAt?: number,
): "syncing" | "synced" | "idle" {
isExpired?: boolean,
): "syncing" | "synced" | "expired" | "idle" {
if (isExpired) return "expired"
if (syncInProgress) return "syncing"
if (lastSyncedAt) return "synced"
return "idle"
@ -16,15 +16,17 @@ function deriveStatus(
interface SyncStatusBadgeProps {
syncInProgress?: boolean
lastSyncedAt?: number
isExpired?: boolean
className?: string
}
export function SyncStatusBadge({
syncInProgress,
lastSyncedAt,
isExpired,
className,
}: SyncStatusBadgeProps) {
const status = deriveStatus(syncInProgress, lastSyncedAt)
const status = deriveStatus(syncInProgress, lastSyncedAt, isExpired)
return (
<div className={cn("flex items-center gap-2", className)}>
@ -33,6 +35,7 @@ export function SyncStatusBadge({
"size-[7px] rounded-full",
status === "syncing" && "bg-[#4BA0FA] animate-pulse",
status === "synced" && "bg-[#00AC3F]",
status === "expired" && "bg-[#EF4444]",
status === "idle" && "bg-[#737373]",
)}
/>
@ -67,6 +70,16 @@ export function SyncStatusBadge({
</span>
</>
)}
{status === "expired" && (
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[16px] tracking-[-0.16px] text-[#EF4444]",
)}
>
Disconnected
</span>
)}
{status === "idle" && (
<span
className={cn(

View file

@ -10,10 +10,16 @@ export function formatRelativeTime(
if (Number.isNaN(d.getTime())) return "Never"
const now = new Date()
const diffMs = now.getTime() - d.getTime()
// Handle future dates (e.g. slight server clock skew)
if (diffMs < 0) return "Just now"
const diffMinutes = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffHours / 24)
if (diffHours < 1) return "Just now"
if (diffMinutes < 1) return "Just now"
if (diffMinutes < 60) return `${diffMinutes}m ago`
if (diffHours < 24) return `${diffHours}h ago`
if (diffDays === 1) return "Yesterday"
if (diffDays < 7) return `${diffDays} days ago`
@ -27,6 +33,17 @@ export const TRIGGER_TYPE_LABELS: Record<string, string> = {
manual: "Manual",
}
/** Canonical provider → display name map. Import this everywhere instead of duplicating. */
export const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
"google-drive": "Google Drive",
notion: "Notion",
onedrive: "OneDrive",
gmail: "Gmail",
github: "GitHub",
"web-crawler": "Web Crawler",
s3: "S3",
}
/** Provider type union matching the backend import endpoint */
export type ImportProvider =
| "google-drive"

View file

@ -3,6 +3,10 @@
import { $fetch } from "@lib/api"
import { useQuery } from "@tanstack/react-query"
/**
* Mirrors the Zod schema at `apiSchema["@get/connections/:connectionId/sync-runs"].output`.
* Keep in sync with `packages/lib/api.ts` if fields are added/removed.
*/
export type SyncRun = {
id: string
connectionId: string
@ -31,5 +35,12 @@ export function useSyncRuns(connectionId: string) {
enabled: !!connectionId,
staleTime: 30 * 1000,
refetchOnMount: "always",
refetchInterval: (query) => {
const runs = query.state.data as SyncRun[] | undefined
if (runs?.some((r) => r.status === "running")) {
return 5000
}
return false
},
})
}

View file

@ -13,6 +13,9 @@ export function useTriggerSync() {
provider,
containerTags,
}: {
// connectionId is not sent to the backend — the import endpoint is keyed
// by provider, so it re-syncs all connections for that provider.
// It's kept here so onSuccess can target cache invalidation.
connectionId: string
provider: ImportProvider
containerTags?: string[]

View file

@ -146,7 +146,7 @@ export const apiSchema = createSchema({
input: z.object({
containerTags: z.array(z.string()).optional(),
}),
output: z.any(),
output: z.unknown(),
params: z.object({
provider: z.enum([
"google-drive",