| 'use client'; |
|
|
| import { useState, useCallback } from 'react'; |
| import { vfs } from '@/lib/vfs'; |
| import { skillsService } from '@/lib/vfs/skills/service'; |
| import { configManager } from '@/lib/config/storage'; |
| import { interviewTemplatesService } from '@/lib/interview/templates-service'; |
| import { getSyncManager } from '@/lib/vfs/sync-manager'; |
| import { |
| DetailedSyncStatus, |
| SyncableItem, |
| CategorySyncStatus, |
| calculateItemSyncStatus, |
| calculateCategoryCounts, |
| } from '@/lib/vfs/sync-types'; |
| import { logger } from '@/lib/utils'; |
|
|
| const EMPTY_CATEGORY: CategorySyncStatus = { |
| items: [], |
| syncedCount: 0, |
| localNewerCount: 0, |
| serverNewerCount: 0, |
| conflictCount: 0, |
| localOnlyCount: 0, |
| serverOnlyCount: 0, |
| }; |
|
|
| const INITIAL_STATUS: DetailedSyncStatus = { |
| projects: EMPTY_CATEGORY, |
| skills: EMPTY_CATEGORY, |
| templates: EMPTY_CATEGORY, |
| modelTemplates: EMPTY_CATEGORY, |
| interviewTemplates: EMPTY_CATEGORY, |
| loading: false, |
| error: null, |
| }; |
|
|
| export function useSyncStatus() { |
| const [status, setStatus] = useState<DetailedSyncStatus>(INITIAL_STATUS); |
| const [refreshing, setRefreshing] = useState(false); |
| const [hasLoaded, setHasLoaded] = useState(false); |
| const syncManager = getSyncManager(); |
|
|
| const fetchStatus = useCallback(async () => { |
| if (hasLoaded) { |
| setRefreshing(true); |
| } else { |
| setStatus((prev) => ({ ...prev, loading: true, error: null })); |
| } |
|
|
| try { |
| |
| const serverResult = await syncManager.getEnhancedSyncStatus(); |
|
|
| if (!serverResult.success || !serverResult.data) { |
| setStatus((prev) => ({ |
| ...prev, |
| loading: false, |
| error: serverResult.error || 'Failed to fetch server status', |
| })); |
| setRefreshing(false); |
| return; |
| } |
|
|
| const serverData = serverResult.data; |
|
|
| |
| const serverProjects = new Map( |
| serverData.projects.map((p) => [p.id, { name: p.name, updatedAt: new Date(p.updatedAt) }]) |
| ); |
| const serverSkills = new Map( |
| serverData.skills.map((s) => [s.id, { name: s.name, updatedAt: new Date(s.updatedAt) }]) |
| ); |
| const serverTemplates = new Map( |
| serverData.templates.map((t) => [t.id, { name: t.name, updatedAt: new Date(t.updatedAt) }]) |
| ); |
| const serverModelTemplates = new Map( |
| (serverData.modelTemplates ?? []).map((t) => [t.id, { name: t.name, updatedAt: new Date(t.updatedAt) }]) |
| ); |
| const serverInterviewTemplates = new Map( |
| (serverData.interviewTemplates ?? []).map((t) => [t.id, { name: t.name, updatedAt: new Date(t.updatedAt) }]) |
| ); |
|
|
| |
| await vfs.init(); |
| const localProjects = await vfs.listProjects(); |
|
|
| const localSkills = await skillsService.getCustomSkills(); |
|
|
| const localTemplates = await vfs.getStorageAdapter().getAllCustomTemplates(); |
|
|
| |
| const projectItems: SyncableItem[] = []; |
| const processedProjectIds = new Set<string>(); |
|
|
| |
| for (const project of localProjects) { |
| processedProjectIds.add(project.id); |
| const serverInfo = serverProjects.get(project.id); |
|
|
| const status = calculateItemSyncStatus( |
| project.updatedAt, |
| serverInfo?.updatedAt || null, |
| project.lastSyncedAt || null |
| ); |
|
|
| projectItems.push({ |
| id: project.id, |
| name: project.name, |
| type: 'project', |
| localUpdatedAt: project.updatedAt, |
| serverUpdatedAt: serverInfo?.updatedAt || null, |
| lastSyncedAt: project.lastSyncedAt || null, |
| status, |
| }); |
| } |
|
|
| |
| for (const [id, info] of serverProjects) { |
| if (!processedProjectIds.has(id)) { |
| projectItems.push({ |
| id, |
| name: info.name, |
| type: 'project', |
| localUpdatedAt: null, |
| serverUpdatedAt: info.updatedAt, |
| lastSyncedAt: null, |
| status: 'server-only', |
| }); |
| } |
| } |
|
|
| |
| const skillItems: SyncableItem[] = []; |
| const processedSkillIds = new Set<string>(); |
|
|
| |
| for (const skill of localSkills) { |
| processedSkillIds.add(skill.id); |
| const serverInfo = serverSkills.get(skill.id); |
|
|
| const status = calculateItemSyncStatus( |
| skill.updatedAt, |
| serverInfo?.updatedAt || null, |
| skill.lastSyncedAt || null |
| ); |
|
|
| skillItems.push({ |
| id: skill.id, |
| name: skill.name, |
| type: 'skill', |
| localUpdatedAt: skill.updatedAt, |
| serverUpdatedAt: serverInfo?.updatedAt || null, |
| lastSyncedAt: skill.lastSyncedAt || null, |
| status, |
| }); |
| } |
|
|
| |
| for (const [id, info] of serverSkills) { |
| if (!processedSkillIds.has(id)) { |
| skillItems.push({ |
| id, |
| name: info.name, |
| type: 'skill', |
| localUpdatedAt: null, |
| serverUpdatedAt: info.updatedAt, |
| lastSyncedAt: null, |
| status: 'server-only', |
| }); |
| } |
| } |
|
|
| |
| const templateItems: SyncableItem[] = []; |
| const processedTemplateIds = new Set<string>(); |
|
|
| |
| for (const template of localTemplates) { |
| processedTemplateIds.add(template.id); |
| const serverInfo = serverTemplates.get(template.id); |
|
|
| |
| const localUpdatedAt = template.updatedAt || template.importedAt; |
|
|
| const status = calculateItemSyncStatus( |
| localUpdatedAt, |
| serverInfo?.updatedAt || null, |
| null |
| ); |
|
|
| templateItems.push({ |
| id: template.id, |
| name: template.name, |
| type: 'template', |
| localUpdatedAt, |
| serverUpdatedAt: serverInfo?.updatedAt || null, |
| lastSyncedAt: null, |
| status, |
| }); |
| } |
|
|
| |
| for (const [id, info] of serverTemplates) { |
| if (!processedTemplateIds.has(id)) { |
| templateItems.push({ |
| id, |
| name: info.name, |
| type: 'template', |
| localUpdatedAt: null, |
| serverUpdatedAt: info.updatedAt, |
| lastSyncedAt: null, |
| status: 'server-only', |
| }); |
| } |
| } |
|
|
| |
| const localModelTemplates = Object.values(configManager.getModelTemplates()).filter((t) => !t.builtin); |
| const modelTemplateItems: SyncableItem[] = []; |
| const processedModelTemplateIds = new Set<string>(); |
|
|
| for (const t of localModelTemplates) { |
| processedModelTemplateIds.add(t.id); |
| const serverInfo = serverModelTemplates.get(t.id); |
| const localUpdatedAt = t.updatedAt || null; |
| const status = calculateItemSyncStatus( |
| localUpdatedAt, |
| serverInfo?.updatedAt || null, |
| t.lastSyncedAt || null |
| ); |
| modelTemplateItems.push({ |
| id: t.id, |
| name: t.name, |
| type: 'modelTemplate', |
| localUpdatedAt, |
| serverUpdatedAt: serverInfo?.updatedAt || null, |
| lastSyncedAt: t.lastSyncedAt || null, |
| status, |
| }); |
| } |
|
|
| for (const [id, info] of serverModelTemplates) { |
| if (!processedModelTemplateIds.has(id)) { |
| modelTemplateItems.push({ |
| id, |
| name: info.name, |
| type: 'modelTemplate', |
| localUpdatedAt: null, |
| serverUpdatedAt: info.updatedAt, |
| lastSyncedAt: null, |
| status: 'server-only', |
| }); |
| } |
| } |
|
|
| |
| const localInterviewTemplates = await interviewTemplatesService.getCustomTemplates(); |
| const interviewTemplateItems: SyncableItem[] = []; |
| const processedInterviewTemplateIds = new Set<string>(); |
|
|
| for (const t of localInterviewTemplates) { |
| processedInterviewTemplateIds.add(t.id); |
| const serverInfo = serverInterviewTemplates.get(t.id); |
| const localUpdatedAt = t.updatedAt || null; |
| const status = calculateItemSyncStatus( |
| localUpdatedAt, |
| serverInfo?.updatedAt || null, |
| t.lastSyncedAt || null |
| ); |
| interviewTemplateItems.push({ |
| id: t.id, |
| name: t.title, |
| type: 'interviewTemplate', |
| localUpdatedAt, |
| serverUpdatedAt: serverInfo?.updatedAt || null, |
| lastSyncedAt: t.lastSyncedAt || null, |
| status, |
| }); |
| } |
|
|
| for (const [id, info] of serverInterviewTemplates) { |
| if (!processedInterviewTemplateIds.has(id)) { |
| interviewTemplateItems.push({ |
| id, |
| name: info.name, |
| type: 'interviewTemplate', |
| localUpdatedAt: null, |
| serverUpdatedAt: info.updatedAt, |
| lastSyncedAt: null, |
| status: 'server-only', |
| }); |
| } |
| } |
|
|
| |
| const projectCounts = calculateCategoryCounts(projectItems); |
| const skillCounts = calculateCategoryCounts(skillItems); |
| const templateCounts = calculateCategoryCounts(templateItems); |
| const modelTemplateCounts = calculateCategoryCounts(modelTemplateItems); |
| const interviewTemplateCounts = calculateCategoryCounts(interviewTemplateItems); |
|
|
| setStatus({ |
| projects: { items: projectItems, ...projectCounts }, |
| skills: { items: skillItems, ...skillCounts }, |
| templates: { items: templateItems, ...templateCounts }, |
| modelTemplates: { items: modelTemplateItems, ...modelTemplateCounts }, |
| interviewTemplates: { items: interviewTemplateItems, ...interviewTemplateCounts }, |
| loading: false, |
| error: null, |
| }); |
| setHasLoaded(true); |
| } catch (error) { |
| logger.error('[useSyncStatus] Error fetching sync status:', error); |
| setStatus((prev) => ({ |
| ...prev, |
| loading: false, |
| error: error instanceof Error ? error.message : 'Failed to fetch sync status', |
| })); |
| } finally { |
| setRefreshing(false); |
| } |
| }, [syncManager, hasLoaded]); |
|
|
| return { |
| status, |
| refresh: fetchStatus, |
| loading: status.loading, |
| refreshing, |
| error: status.error, |
| }; |
| } |
|
|