Show UI drift marker for env draft vs Vercel config

This commit is contained in:
CJACK.
2026-03-21 17:08:43 +08:00
parent 1e7e0b2ae3
commit 43a6e6712f
12 changed files with 131 additions and 15 deletions

View File

@@ -101,6 +101,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
onPageSizeChange={changePageSize}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
envBacked={Boolean(config?.env_backed)}
/>
<AddKeyModal

View File

@@ -26,6 +26,7 @@ export default function AccountsTable({
onPageSizeChange,
searchQuery,
onSearchChange,
envBacked = false,
}) {
const [copiedId, setCopiedId] = useState(null)
@@ -101,14 +102,16 @@ export default function AccountsTable({
) : accounts.length > 0 ? (
accounts.map((acc, i) => {
const id = resolveAccountIdentifier(acc)
const runtimeUnknown = envBacked && !acc.test_status
const isActive = acc.test_status === 'ok' || acc.has_token
return (
<div key={i} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-3 min-w-0">
<div className={clsx(
"w-2 h-2 rounded-full shrink-0",
acc.test_status === 'failed' ? "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" :
(acc.test_status === 'ok' || acc.has_token) ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
"bg-amber-500"
isActive ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
runtimeUnknown ? "bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]" : "bg-amber-500"
)} />
<div className="min-w-0">
<div
@@ -122,7 +125,7 @@ export default function AccountsTable({
}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : isActive ? t('accountManager.sessionActive') : runtimeUnknown ? t('accountManager.runtimeStatusUnknown') : t('accountManager.reauthRequired')}</span>
{acc.token_preview && (
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
{acc.token_preview}

View File

@@ -4,7 +4,7 @@ import VercelSyncForm from './VercelSyncForm'
import VercelSyncStatus from './VercelSyncStatus'
import VercelGuide from './VercelGuide'
export default function VercelSyncContainer({ onMessage, authFetch, isVercel = false }) {
export default function VercelSyncContainer({ onMessage, authFetch, isVercel = false, config = null }) {
const { t } = useI18n()
const apiFetch = authFetch || fetch
@@ -28,6 +28,7 @@ export default function VercelSyncContainer({ onMessage, authFetch, isVercel = f
onMessage,
t,
isVercel,
config,
})
return (

View File

@@ -69,6 +69,11 @@ export default function VercelSyncForm({
{t('vercel.lastSyncTime', { time: new Date(syncStatus.last_sync_time * 1000).toLocaleString() })}
</p>
)}
{syncStatus?.draft_differs && (
<p className="text-xs text-amber-500 mt-2">
{t('vercel.draftDiffers')}
</p>
)}
</div>
<div className="space-y-4">

View File

@@ -8,7 +8,7 @@ function pollDelayMs(attempt) {
return 60000
}
export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false }) {
export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false, config = null }) {
const [vercelToken, setVercelToken] = useState('')
const [projectId, setProjectId] = useState('')
const [teamId, setTeamId] = useState('')
@@ -22,7 +22,14 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
const fetchSyncStatus = useCallback(async ({ manual = false } = {}) => {
try {
const res = await apiFetch('/admin/vercel/status')
const hasConfig = Boolean(config && (Array.isArray(config?.keys) || Array.isArray(config?.accounts)))
const res = await apiFetch('/admin/vercel/status', hasConfig ? {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config_override: { keys: config?.keys || [], accounts: config?.accounts || [] },
}),
} : undefined)
if (!res.ok) {
throw new Error(`status ${res.status}`)
}
@@ -50,7 +57,7 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
// eslint-disable-next-line no-console
console.error('Failed to fetch sync status:', e)
}
}, [apiFetch, isVercel, onMessage, t])
}, [apiFetch, config, isVercel, onMessage, t])
useEffect(() => {
const loadPreconfig = async () => {
@@ -117,6 +124,7 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
vercel_token: tokenToUse,
project_id: projectId,
team_id: teamId || undefined,
config_override: config ? { keys: config.keys || [], accounts: config.accounts || [] } : undefined,
}),
})
const data = await res.json()
@@ -133,7 +141,7 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
} finally {
setLoading(false)
}
}, [apiFetch, fetchSyncStatus, onMessage, preconfig?.has_token, projectId, t, teamId, vercelToken])
}, [apiFetch, config, fetchSyncStatus, onMessage, preconfig?.has_token, projectId, t, teamId, vercelToken])
return {
vercelToken,