/** * Sync Manager * * Handles synchronization between browser (IndexedDB) and server (SQLite) in Server mode. * Provides methods to push local data to server and pull server data to browser. */ import { Project, VirtualFile, CustomTemplate } from './types'; import { Skill } from './skills/types'; import type { ModelTemplate } from '@/lib/llm/models/assignment'; import type { CustomConnection } from '@/lib/llm/providers/connection-record'; import { EnhancedSyncStatusResponse } from './sync-types'; export interface SyncResult { success: boolean; error?: string; } export interface ProjectSyncResult extends SyncResult { project?: Project; } export interface FilesSyncResult extends SyncResult { count?: number; } export interface ProjectListSyncResult extends SyncResult { projects?: Project[]; } export interface FilesListSyncResult extends SyncResult { files?: VirtualFile[]; } export interface SkillSyncResult extends SyncResult { skill?: Skill; action?: 'created' | 'updated'; } export interface SkillsListSyncResult extends SyncResult { skills?: Skill[]; created?: number; updated?: number; } export interface TemplateSyncResult extends SyncResult { template?: CustomTemplate; action?: 'created' | 'updated'; } export interface TemplatesListSyncResult extends SyncResult { templates?: CustomTemplate[]; created?: number; updated?: number; } export interface ModelTemplateSyncResult extends SyncResult { modelTemplate?: ModelTemplate; action?: 'created' | 'updated'; } export interface ModelTemplatesListSyncResult extends SyncResult { modelTemplates?: ModelTemplate[]; created?: number; updated?: number; } export interface InterviewTemplateSyncResult extends SyncResult { interviewTemplate?: import('@/lib/interview/types').InterviewTemplate; action?: 'created' | 'updated'; } export interface InterviewTemplatesListSyncResult extends SyncResult { interviewTemplates?: import('@/lib/interview/types').InterviewTemplate[]; created?: number; updated?: number; } export interface ConnectionSyncResult extends SyncResult { connection?: CustomConnection; action?: 'created' | 'updated'; } export interface ConnectionsListSyncResult extends SyncResult { connections?: CustomConnection[]; } // Helper: Convert ArrayBuffer to base64 for JSON transport function serializeFileContent(file: VirtualFile): VirtualFile & { _isBinaryBase64?: boolean } { if (file.content instanceof ArrayBuffer) { const bytes = new Uint8Array(file.content); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return { ...file, content: btoa(binary), _isBinaryBase64: true }; } return file; } // Helper: Convert base64 back to ArrayBuffer after JSON transport function deserializeFileContent(file: VirtualFile & { _isBinaryBase64?: boolean }): VirtualFile { if (file._isBinaryBase64 && typeof file.content === 'string') { const binaryString = atob(file.content); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const { _isBinaryBase64, ...rest } = file; return { ...rest, content: bytes.buffer }; } const { _isBinaryBase64, ...rest } = file; return rest; } /** * SyncManager - Client-side sync utility for Server mode */ export class SyncManager { private baseUrl: string; workspaceId?: string; constructor(baseUrl = '') { this.baseUrl = baseUrl; } /** * Build an API URL, scoped to a workspace when workspaceId is set. * * With workspaceId: /api/w/{workspaceId}{path} * Without: /api{path} * * @param path - must start with '/' (e.g. '/sync/projects') */ private getApiUrl(path: string): string { if (this.workspaceId) { return `/api/w/${this.workspaceId}${path}`; } return `/api${path}`; } /** * Push project to server (IndexedDB -> SQLite) */ async pushProject(project: Project): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/projects')}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ project }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, project: data.project, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Pull all projects from server (SQLite -> IndexedDB) */ async pullProjects(): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/projects')}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, projects: data.projects, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Push files for a project to server (IndexedDB -> SQLite) */ async pushFiles(projectId: string, files: VirtualFile[]): Promise { try { const serializedFiles = files.map(serializeFileContent); const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/files')}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ projectId, files: serializedFiles }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, count: data.count, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Pull files for a project from server (SQLite -> IndexedDB) */ async pullFiles(projectId: string): Promise { try { const response = await fetch( `${this.baseUrl}${this.getApiUrl('/sync/files')}?projectId=${encodeURIComponent(projectId)}`, { method: 'GET', } ); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, files: (data.files || []).map(deserializeFileContent), }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Full project sync: push project + files to server */ async pushProjectWithFiles( project: Project, files: VirtualFile[] ): Promise { // Push project metadata first const projectResult = await this.pushProject(project); if (!projectResult.success) { return projectResult; } // Then push all files const filesResult = await this.pushFiles(project.id, files); if (!filesResult.success) { return filesResult; } return { success: true }; } /** * Full project sync: pull project + files from server to IndexedDB * Returns the project and files for the caller to insert into IndexedDB */ async pullProjectWithFiles(projectId: string): Promise<{ success: boolean; error?: string; project?: Project; files?: VirtualFile[]; }> { // Pull all projects and find the one we need const projectsResult = await this.pullProjects(); if (!projectsResult.success || !projectsResult.projects) { return { success: false, error: projectsResult.error || 'Failed to pull projects', }; } const project = projectsResult.projects.find((p) => p.id === projectId); if (!project) { return { success: false, error: `Project ${projectId} not found on server`, }; } // Pull files for the project const filesResult = await this.pullFiles(projectId); if (!filesResult.success) { return { success: false, error: filesResult.error || 'Failed to pull files', }; } return { success: true, project, files: filesResult.files || [], }; } /** * Push single project to server (new API endpoint) */ async pushSingleProject(projectId: string, project: Project, files: VirtualFile[]): Promise { try { const serializedFiles = files.map(serializeFileContent); const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/projects/${projectId}`)}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ project, files: serializedFiles }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, project: data.project, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Pull single project from server (new API endpoint) */ async pullSingleProject(projectId: string): Promise<{ success: boolean; error?: string; project?: Project; files?: VirtualFile[]; }> { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/projects/${projectId}`)}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, project: data.project, files: (data.files || []).map(deserializeFileContent), }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Get sync status for all projects on server */ async getSyncStatus(): Promise<{ success: boolean; error?: string; projects?: Array<{ id: string; updatedAt: string }>; }> { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/status')}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, projects: data.projects || [], }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } // ============================================ // Skills Sync Methods // ============================================ /** * Pull all custom skills from server */ async pullSkills(): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/skills')}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, skills: data.skills || [], }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Push multiple skills to server */ async pushSkills(skills: Skill[]): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/skills')}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ skills }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: data.success, created: data.created, updated: data.updated, error: data.errors?.join(', '), }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Pull a single skill from server */ async pullSkill(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/skills/${encodeURIComponent(id)}`)}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, skill: data.skill, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Push a single skill to server */ async pushSkill(skill: Skill): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/skills/${encodeURIComponent(skill.id)}`)}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ skill }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, skill: data.skill, action: data.action, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Delete a skill from server */ async deleteSkillFromServer(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/skills/${encodeURIComponent(id)}`)}`, { method: 'DELETE', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } // ============================================ // Model Template Sync Methods // ============================================ /** Pull all model templates from server */ async pullModelTemplates(): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/model-templates')}`, { method: 'GET' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, modelTemplates: data.modelTemplates || [] }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Push multiple model templates to server */ async pushModelTemplates(modelTemplates: ModelTemplate[]): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/model-templates')}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelTemplates }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: data.success, created: data.created, updated: data.updated, error: data.errors?.join(', ') }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Pull a single model template from server */ async pullModelTemplate(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/model-templates/${encodeURIComponent(id)}`)}`, { method: 'GET' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, modelTemplate: data.modelTemplate }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Push a single model template to server */ async pushModelTemplate(modelTemplate: ModelTemplate): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/model-templates/${encodeURIComponent(modelTemplate.id)}`)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelTemplate }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, modelTemplate: data.modelTemplate, action: data.action }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Delete a model template from server */ async deleteModelTemplateFromServer(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/model-templates/${encodeURIComponent(id)}`)}`, { method: 'DELETE' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } // ============================================ // Interview Template Sync Methods // ============================================ /** Pull all interview templates from server */ async pullInterviewTemplates(): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/interview-templates')}`, { method: 'GET' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, interviewTemplates: data.interviewTemplates || [] }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Push multiple interview templates to server */ async pushInterviewTemplates(interviewTemplates: import('@/lib/interview/types').InterviewTemplate[]): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/interview-templates')}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interviewTemplates }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: data.success, created: data.created, updated: data.updated, error: data.errors?.join(', ') }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Pull a single interview template from server */ async pullInterviewTemplate(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/interview-templates/${encodeURIComponent(id)}`)}`, { method: 'GET' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, interviewTemplate: data.interviewTemplate }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Push a single interview template to server */ async pushInterviewTemplate(interviewTemplate: import('@/lib/interview/types').InterviewTemplate): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/interview-templates/${encodeURIComponent(interviewTemplate.id)}`)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interviewTemplate }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, interviewTemplate: data.interviewTemplate, action: data.action }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Delete an interview template from server */ async deleteInterviewTemplateFromServer(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/interview-templates/${encodeURIComponent(id)}`)}`, { method: 'DELETE' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } // ============================================ // Connection Sync Methods // ============================================ /** Pull all custom connections from server */ async pullConnections(): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/connections')}`, { method: 'GET' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, connections: data.connections || [] }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Push a single custom connection to server */ async pushConnection(connection: CustomConnection): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/connections/${encodeURIComponent(connection.id)}`)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ connection }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, connection: data.connection, action: data.action }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** Delete a custom connection from server */ async deleteConnectionFromServer(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/connections/${encodeURIComponent(id)}`)}`, { method: 'DELETE' }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } // ============================================ // Templates Sync Methods // ============================================ /** * Pull all custom templates from server */ async pullTemplates(): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/templates')}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, templates: data.templates || [], }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Push multiple templates to server */ async pushTemplates(templates: CustomTemplate[]): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/templates')}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ templates }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: data.success, created: data.created, updated: data.updated, error: data.errors?.join(', '), }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Pull a single template from server */ async pullTemplate(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/templates/${encodeURIComponent(id)}`)}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, template: data.template, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Push a single template to server */ async pushTemplate(template: CustomTemplate): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/templates/${encodeURIComponent(template.id)}`)}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ template }), }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, template: data.template, action: data.action, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } /** * Delete a template from server */ async deleteTemplateFromServer(id: string): Promise { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/templates/${encodeURIComponent(id)}`)}`, { method: 'DELETE', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } // ============================================ // Backend Features Sync (Project-scoped) // ============================================ /** * Push backend features from IndexedDB to server (core SQLite) * Called after backend feature modifications in the workspace */ async pushBackendFeatures( projectId: string, features?: { edgeFunctions: import('./types').EdgeFunction[]; serverFunctions: import('./types').ServerFunction[]; secrets: import('./types').Secret[]; scheduledFunctions: import('./types').ScheduledFunction[]; } ): Promise<{ success: boolean; error?: string }> { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/backend-features/${projectId}`)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(features || { edgeFunctions: [], serverFunctions: [], secrets: [], scheduledFunctions: [], }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } /** * Pull backend features from server (core SQLite) to IndexedDB * Called during project sync/pull operations */ async pullBackendFeatures(projectId: string): Promise<{ success: boolean; error?: string }> { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl(`/sync/backend-features/${projectId}`)}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); return { success: false, error: errorData.error || `HTTP ${response.status}` }; } const data = await response.json() as { edgeFunctions?: import('./types').EdgeFunction[]; serverFunctions?: import('./types').ServerFunction[]; secrets?: import('./types').Secret[]; scheduledFunctions?: import('./types').ScheduledFunction[]; }; // Write pulled features to IndexedDB via the VFS adapter try { const { vfs } = await import('@/lib/vfs'); const adapter = vfs.getStorageAdapter(); // Clear existing features for this project, then re-create from server data if (adapter.listEdgeFunctions && adapter.deleteEdgeFunction && adapter.createEdgeFunction) { const existing = await adapter.listEdgeFunctions(projectId); for (const fn of existing) { await adapter.deleteEdgeFunction(fn.id); } for (const fn of data.edgeFunctions || []) { await adapter.createEdgeFunction({ ...fn, projectId }); } } if (adapter.listServerFunctions && adapter.deleteServerFunction && adapter.createServerFunction) { const existing = await adapter.listServerFunctions(projectId); for (const fn of existing) { await adapter.deleteServerFunction(fn.id); } for (const fn of data.serverFunctions || []) { await adapter.createServerFunction({ ...fn, projectId }); } } if (adapter.listSecrets && adapter.deleteSecret && adapter.createSecret) { const existing = await adapter.listSecrets(projectId); for (const s of existing) { await adapter.deleteSecret(s.id); } for (const s of data.secrets || []) { await adapter.createSecret({ ...s, projectId }); } } if (adapter.listScheduledFunctions && adapter.deleteScheduledFunction && adapter.createScheduledFunction) { const existing = await adapter.listScheduledFunctions(projectId); for (const fn of existing) { await adapter.deleteScheduledFunction(fn.id); } for (const fn of data.scheduledFunctions || []) { await adapter.createScheduledFunction({ ...fn, projectId }); } } } catch (writeError) { console.error('[SyncManager] Failed to write pulled backend features to IndexedDB:', writeError); return { success: false, error: writeError instanceof Error ? writeError.message : 'Failed to write to IndexedDB' }; } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error' }; } } // ============================================ // Detailed Sync Status // ============================================ /** * Get enhanced sync status including skills and templates */ async getEnhancedSyncStatus(): Promise<{ success: boolean; error?: string; data?: EnhancedSyncStatusResponse; }> { try { const response = await fetch(`${this.baseUrl}${this.getApiUrl('/sync/status')}`, { method: 'GET', }); if (!response.ok) { const errorData = await response.json(); return { success: false, error: errorData.error || `HTTP ${response.status}`, }; } const data = await response.json(); return { success: true, data: data as EnhancedSyncStatusResponse, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } } /** * Global singleton instance */ let syncManager: SyncManager | null = null; /** * Get or create SyncManager instance. * * When workspaceId is provided the singleton's workspaceId is updated so all * subsequent API calls are scoped to `/api/w/{workspaceId}/…`. Pass * `undefined` (or omit) to keep the current workspaceId unchanged, or pass * `null` explicitly to clear it (reverts to unscoped `/api/…` paths). */ export function getSyncManager(workspaceId?: string | null): SyncManager { if (!syncManager) { syncManager = new SyncManager(); } if (workspaceId !== undefined) { syncManager.workspaceId = workspaceId ?? undefined; } return syncManager; }