Files
ccs/ui/src/hooks/use-cliproxy-sync.ts
T
Tam Nhu Tran c3401f0a91 feat(ui): complete dashboard i18n coverage across 162 files
Replace all remaining hardcoded English strings with t() calls across
the CCS dashboard UI. Add zh-CN, vi, ja translations for all new keys.

- 162 files updated (components, hooks, pages, lib utilities)
- 7 domains covered: layout/shared, accounts/auth, CLIProxy,
  compatible CLI (Codex/Cursor/Droid/Copilot), logs/monitoring,
  analytics, and lib utilities
- 4 locales: en, zh-CN, vi, ja with natural (not literal) translations
- Fix duplicate customPresetDialog key shadowing in en/zh-CN
- Fix missing ja nav.logs and providerModelSelector keys
- Fix openrouter-banner count type (string → number)
- Fix add-account-dialog double setLocalError call
- Fix codex-model-providers-card missing useTranslation

Closes #983
2026-04-13 20:52:30 -04:00

179 lines
4.3 KiB
TypeScript

/**
* React Query hooks for CLIProxy sync functionality
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
/** Sync status response */
export interface SyncStatus {
connected: boolean;
configured: boolean;
remoteUrl?: string;
latencyMs?: number;
version?: string;
error?: string;
errorCode?: string;
}
/** Sync preview item */
export interface SyncPreviewItem {
name: string;
baseUrl?: string;
modelName?: string;
}
/** Masked payload item for preview */
interface MaskedPayloadItem {
'api-key': string;
'base-url'?: string;
prefix?: string;
models?: { name: string; alias: string }[];
}
/** Sync preview response */
export interface SyncPreview {
profiles: SyncPreviewItem[];
payload: MaskedPayloadItem[];
count: number;
}
/** Sync result response */
export interface SyncResult {
success: boolean;
syncedCount?: number;
remoteUrl?: string;
profiles?: string[];
error?: string;
errorCode?: string;
message?: string;
}
/**
* Fetch sync status from API
*/
async function fetchSyncStatus(): Promise<SyncStatus> {
const response = await fetch('/api/cliproxy/sync/status');
if (!response.ok) {
let message = 'Failed to fetch sync status';
try {
const error = await response.json();
message = error.error || error.message || message;
} catch {
// Non-JSON response (e.g., 502 Bad Gateway)
}
throw new Error(message);
}
return response.json();
}
/**
* Fetch sync preview from API
*/
async function fetchSyncPreview(): Promise<SyncPreview> {
const response = await fetch('/api/cliproxy/sync/preview');
if (!response.ok) {
let message = 'Failed to fetch sync preview';
try {
const error = await response.json();
message = error.error || error.message || message;
} catch {
// Non-JSON response (e.g., 502 Bad Gateway)
}
throw new Error(message);
}
return response.json();
}
/**
* Execute sync to remote CLIProxy
*/
async function executeSync(): Promise<SyncResult> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch('/api/cliproxy/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
});
if (!response.ok) {
let message = 'Sync failed';
try {
const data = await response.json();
message = data.error || data.message || message;
} catch {
// Non-JSON response (e.g., 502 Bad Gateway)
}
throw new Error(message);
}
return response.json();
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Sync request timed out after 30 seconds');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Hook to get sync status
*/
export function useSyncStatus() {
return useQuery({
queryKey: ['cliproxy-sync-status'],
queryFn: fetchSyncStatus,
refetchInterval: 30000, // Check every 30 seconds
retry: 1,
staleTime: 10000,
});
}
/**
* Hook to get sync preview
*/
export function useSyncPreview() {
return useQuery({
queryKey: ['cliproxy-sync-preview'],
queryFn: fetchSyncPreview,
staleTime: 5000,
retry: 1,
});
}
/**
* Hook to execute sync with toast feedback
*/
export function useExecuteSync() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: executeSync,
onSuccess: (data) => {
// Invalidate sync-related queries after successful sync
queryClient.invalidateQueries({ queryKey: ['cliproxy-sync-status'] });
queryClient.invalidateQueries({ queryKey: ['cliproxy-sync-preview'] });
// Show success toast with synced count
if (data.syncedCount === 0) {
toast.info(t('toasts.noProfilesToSync'));
} else {
// TODO i18n: missing key for 'Synced {{count}} profile(s) to CLIProxy'
toast.success(
`Synced ${data.syncedCount} profile${data.syncedCount === 1 ? '' : 's'} to CLIProxy`
);
}
},
onError: (error: Error) => {
toast.error(t('toasts.syncFailed', { error: error.message }));
},
});
}