From a8499626c0780d50d8ad3f025dd2316b5c6cec35 Mon Sep 17 00:00:00 2001 From: hugo Date: Wed, 11 Feb 2026 07:45:45 +0100 Subject: [PATCH] broken: halfway through removing turso --- .github/copilot-instructions.md | 19 +- src/main/database/connection.ts | 47 -- src/main/engine/SyncEngine.ts | 324 +++++--------- src/renderer/App.tsx | 12 +- .../CredentialsPanel/CredentialsPanel.tsx | 88 +--- .../components/SettingsView/SettingsView.tsx | 129 +----- src/renderer/types/electron.d.ts | 2 - tests/engine/SyncEngine.test.ts | 405 ++++++++++++++++++ tests/engine/TaskManager.test.ts | 4 + 9 files changed, 561 insertions(+), 469 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0a399c3..92fd6e1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,7 +9,7 @@ This document provides context and best practices for GitHub Copilot when workin - **TypeScript** for all code (strict mode) - **React** for the renderer UI - **Drizzle ORM** for type-safe database access -- **@libsql/client** for SQLite (local) and Turso (cloud sync) +- **@libsql/client** for SQLite (local database) - **Zustand** for React state management --- @@ -32,6 +32,19 @@ See the [TDD Requirements](#test-driven-development-tdd-requirements) section fo --- +## ⚠️ MANDATORY: Fix All Test Failures + +**You MUST investigate and fix ALL test failures before completing any task.** + +- Never leave tests failing, even if they appear unrelated to your changes +- If a test failure is pre-existing, fix it as part of your current work +- Run the full test suite (`npm test`) before considering any task complete +- If you cannot fix a test, explain why and propose a solution + +> **Zero failing tests. No exceptions.** + +--- + ## Architecture Principles ### Separation of Concerns @@ -240,7 +253,7 @@ app.on('before-quit', async () => { }); ``` -## Remote Sync Best Practices (Turso/LibSQL) +## Remote Sync Best Practices (Dropbox) ### Sync Strategy @@ -719,5 +732,5 @@ export type NewTableInsert = typeof newTable.$inferInsert; - Never log sensitive data (auth tokens, passwords) - Validate all IPC inputs before processing - Use `contextIsolation: true` and `sandbox: false` only when necessary -- Store Turso auth tokens in secure storage, not in code +- Store Dropbox auth tokens in secure storage, not in code - Sanitize user input before rendering (XSS prevention) diff --git a/src/main/database/connection.ts b/src/main/database/connection.ts index a16e6ae..3b28697 100644 --- a/src/main/database/connection.ts +++ b/src/main/database/connection.ts @@ -7,17 +7,13 @@ import * as fs from 'fs'; export interface DatabaseConfig { localPath: string; - tursoUrl?: string; - tursoAuthToken?: string; } type DrizzleDB = ReturnType; export class DatabaseConnection { private localDb: DrizzleDB | null = null; - private remoteDb: DrizzleDB | null = null; private localClient: Client | null = null; - private remoteClient: Client | null = null; private config: DatabaseConfig; constructor(config?: Partial) { @@ -25,8 +21,6 @@ export class DatabaseConnection { this.config = { localPath: config?.localPath || path.join(userDataPath, 'bds.db'), - tursoUrl: config?.tursoUrl, - tursoAuthToken: config?.tursoAuthToken, }; // Ensure user data directory exists @@ -64,38 +58,6 @@ export class DatabaseConnection { return this.localDb; } - async initializeRemote(remoteConfig?: { tursoUrl: string; tursoAuthToken: string }): Promise { - // Update config if new credentials are provided - if (remoteConfig) { - // Close existing remote connection if credentials changed - if (this.remoteClient && - (this.config.tursoUrl !== remoteConfig.tursoUrl || - this.config.tursoAuthToken !== remoteConfig.tursoAuthToken)) { - this.remoteClient.close(); - this.remoteClient = null; - this.remoteDb = null; - } - this.config.tursoUrl = remoteConfig.tursoUrl; - this.config.tursoAuthToken = remoteConfig.tursoAuthToken; - } - - if (!this.config.tursoUrl || !this.config.tursoAuthToken) { - return null; - } - - if (this.remoteDb) { - return this.remoteDb; - } - - this.remoteClient = createClient({ - url: this.config.tursoUrl, - authToken: this.config.tursoAuthToken, - }); - - this.remoteDb = drizzle(this.remoteClient, { schema }); - return this.remoteDb; - } - getLocal(): DrizzleDB { if (!this.localDb) { throw new Error('Local database not initialized. Call initializeLocal() first.'); @@ -103,10 +65,6 @@ export class DatabaseConnection { return this.localDb; } - getRemote(): DrizzleDB | null { - return this.remoteDb; - } - getLocalClient(): Client | null { return this.localClient; } @@ -389,11 +347,6 @@ export class DatabaseConnection { this.localClient = null; this.localDb = null; } - if (this.remoteClient) { - this.remoteClient.close(); - this.remoteClient = null; - this.remoteDb = null; - } } getDataPaths() { diff --git a/src/main/engine/SyncEngine.ts b/src/main/engine/SyncEngine.ts index c224d6a..10f284c 100644 --- a/src/main/engine/SyncEngine.ts +++ b/src/main/engine/SyncEngine.ts @@ -1,19 +1,15 @@ import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; -import { eq, and } from 'drizzle-orm'; +import { eq } from 'drizzle-orm'; import { getDatabase } from '../database'; import { syncLog, posts, media, NewSyncLogEntry } from '../database/schema'; import { taskManager, Task } from './TaskManager'; -import { getPostEngine } from './PostEngine'; -import { getMediaEngine } from './MediaEngine'; import { getDropboxSyncEngine } from './DropboxSyncEngine'; export type SyncDirection = 'push' | 'pull' | 'bidirectional'; export type SyncStatus = 'idle' | 'syncing' | 'error'; export interface SyncConfig { - tursoUrl: string; - tursoAuthToken: string; autoSync: boolean; syncInterval: number; // in minutes } @@ -33,23 +29,37 @@ export interface SyncResult { }; } +// Default timeout for sync operations (30 seconds) +const DEFAULT_SYNC_TIMEOUT = 30000; + export class SyncEngine extends EventEmitter { private syncStatus: SyncStatus = 'idle'; private syncConfig: SyncConfig | null = null; private syncIntervalId: NodeJS.Timeout | null = null; + private syncTimeout: number = DEFAULT_SYNC_TIMEOUT; constructor() { super(); } + getSyncTimeout(): number { + return this.syncTimeout; + } + + setSyncTimeout(timeoutMs: number): void { + this.syncTimeout = timeoutMs; + } + getSyncStatus(): SyncStatus { return this.syncStatus; } + /** + * Check if sync is configured. Uses Dropbox configuration status. + */ isConfigured(): boolean { - return this.syncConfig !== null && - !!this.syncConfig.tursoUrl && - !!this.syncConfig.tursoAuthToken; + const dropboxEngine = getDropboxSyncEngine(); + return dropboxEngine.isConfigured(); } async configure(config: SyncConfig): Promise { @@ -69,179 +79,9 @@ export class SyncEngine extends EventEmitter { ); } - // Initialize remote database connection with the provided credentials - const db = getDatabase(); - await db.initializeRemote({ - tursoUrl: config.tursoUrl, - tursoAuthToken: config.tursoAuthToken, - }); - this.emit('configured', config); } - async sync(direction: SyncDirection = 'bidirectional'): Promise { - if (!this.isConfigured()) { - return { - success: false, - pushed: 0, - pulled: 0, - conflicts: 0, - errors: ['Sync not configured'], - }; - } - - if (this.syncStatus === 'syncing') { - return { - success: false, - pushed: 0, - pulled: 0, - conflicts: 0, - errors: ['Sync already in progress'], - }; - } - - const task: Task = { - id: uuidv4(), - name: `Sync (${direction})`, - execute: async (onProgress) => { - this.syncStatus = 'syncing'; - this.emit('syncStarted', direction); - - const result: SyncResult = { - success: true, - pushed: 0, - pulled: 0, - conflicts: 0, - errors: [], - }; - - try { - const db = getDatabase(); - const localDb = db.getLocal(); - const remoteDb = db.getRemote(); - - if (!remoteDb) { - throw new Error('Remote database not initialized'); - } - - onProgress(10, 'Fetching pending changes...'); - - if (direction === 'push' || direction === 'bidirectional') { - // Get pending posts - const pendingPosts = await localDb - .select() - .from(posts) - .where(eq(posts.syncStatus, 'pending')) - .all(); - - onProgress(20, `Pushing ${pendingPosts.length} posts...`); - - for (const post of pendingPosts) { - try { - // Push to remote (simplified - in production would handle conflicts) - await remoteDb.insert(posts).values(post).onConflictDoUpdate({ - target: posts.id, - set: { - title: post.title, - slug: post.slug, - excerpt: post.excerpt, - status: post.status, - author: post.author, - updatedAt: post.updatedAt, - publishedAt: post.publishedAt, - checksum: post.checksum, - tags: post.tags, - categories: post.categories, - }, - }); - - // Mark as synced locally - await localDb - .update(posts) - .set({ syncStatus: 'synced', syncedAt: new Date() }) - .where(eq(posts.id, post.id)); - - result.pushed++; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : 'Unknown error'; - result.errors.push(`Failed to push post ${post.id}: ${errorMsg}`); - - // Log the error - await this.logSyncOperation(post.id, 'post', 'update', 'failed', errorMsg); - } - } - - // Get pending media - const pendingMedia = await localDb - .select() - .from(media) - .where(eq(media.syncStatus, 'pending')) - .all(); - - onProgress(50, `Pushing ${pendingMedia.length} media items...`); - - for (const item of pendingMedia) { - try { - await remoteDb.insert(media).values(item).onConflictDoUpdate({ - target: media.id, - set: { - alt: item.alt, - caption: item.caption, - updatedAt: item.updatedAt, - checksum: item.checksum, - tags: item.tags, - }, - }); - - await localDb - .update(media) - .set({ syncStatus: 'synced', syncedAt: new Date() }) - .where(eq(media.id, item.id)); - - result.pushed++; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : 'Unknown error'; - result.errors.push(`Failed to push media ${item.id}: ${errorMsg}`); - - await this.logSyncOperation(item.id, 'media', 'update', 'failed', errorMsg); - } - } - } - - if (direction === 'pull' || direction === 'bidirectional') { - onProgress(70, 'Pulling remote changes...'); - - // In a real implementation, we would: - // 1. Fetch all remote records with syncedAt > local last sync - // 2. Compare checksums to detect conflicts - // 3. Apply or merge changes - - // For now, this is a placeholder - onProgress(90, 'Pull complete'); - } - - onProgress(100, 'Sync complete'); - - this.syncStatus = 'idle'; - this.emit('syncCompleted', result); - - return result; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : 'Unknown error'; - result.success = false; - result.errors.push(errorMsg); - - this.syncStatus = 'error'; - this.emit('syncFailed', errorMsg); - - return result; - } - }, - }; - - return taskManager.runTask(task); - } - private async logSyncOperation( entityId: string, entityType: 'post' | 'media', @@ -324,38 +164,110 @@ export class SyncEngine extends EventEmitter { } /** - * Full sync: metadata via Turso + files via Dropbox. - * Coordinates both sync engines for a complete bidirectional sync. + * Full sync: Files via Dropbox. + * Synchronizes posts and media files to Dropbox for backup and cross-device access. */ async fullSync(direction: SyncDirection = 'bidirectional'): Promise { - // Run metadata sync (Turso) - const metadataResult = await this.sync(direction); - - // Run file sync (Dropbox) if configured - const dropboxEngine = getDropboxSyncEngine(); - if (dropboxEngine.isConfigured()) { - try { - const fileResult = await dropboxEngine.syncAll(); - metadataResult.dropboxResult = { - uploaded: fileResult.uploaded, - downloaded: fileResult.downloaded, - deleted: fileResult.deleted, - conflicts: fileResult.conflicts, - errors: fileResult.errors, - }; - - if (!fileResult.success) { - metadataResult.errors.push( - ...fileResult.errors.map(e => `Dropbox: ${e}`) - ); - } - } catch (error) { - const msg = error instanceof Error ? error.message : 'Unknown Dropbox error'; - metadataResult.errors.push(`Dropbox sync failed: ${msg}`); - } + if (this.syncStatus === 'syncing') { + return { + success: false, + pushed: 0, + pulled: 0, + conflicts: 0, + errors: ['Sync already in progress'], + }; } - return metadataResult; + const result: SyncResult = { + success: true, + pushed: 0, + pulled: 0, + conflicts: 0, + errors: [], + }; + + const dropboxEngine = getDropboxSyncEngine(); + if (!dropboxEngine.isConfigured()) { + return { + success: false, + pushed: 0, + pulled: 0, + conflicts: 0, + errors: ['Dropbox sync not configured'], + }; + } + + console.log('[SyncEngine] Starting Dropbox file sync...'); + + const task: Task = { + id: uuidv4(), + name: `Sync (${direction})`, + execute: async (onProgress) => { + this.syncStatus = 'syncing'; + this.emit('syncStarted', direction); + + try { + onProgress(10, 'Starting Dropbox sync...'); + + const fileResult = await dropboxEngine.syncAll(); + + result.dropboxResult = { + uploaded: fileResult.uploaded, + downloaded: fileResult.downloaded, + deleted: fileResult.deleted, + conflicts: fileResult.conflicts, + errors: fileResult.errors, + }; + + result.pushed = fileResult.uploaded; + result.pulled = fileResult.downloaded; + result.conflicts = fileResult.conflicts; + + if (!fileResult.success) { + result.success = false; + result.errors.push(...fileResult.errors.map(e => `Dropbox: ${e}`)); + } + + onProgress(100, 'Sync complete'); + console.log('[SyncEngine] Dropbox sync complete:', { + uploaded: fileResult.uploaded, + downloaded: fileResult.downloaded, + errors: fileResult.errors.length, + }); + + this.syncStatus = 'idle'; + this.emit('syncCompleted', result); + + return result; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + console.error('[SyncEngine] Sync failed:', errorMsg); + result.success = false; + result.errors.push(`Dropbox sync failed: ${errorMsg}`); + + this.syncStatus = 'error'; + this.emit('syncFailed', errorMsg); + + return result; + } + }, + }; + + try { + return await taskManager.runTask(task); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + console.error('[SyncEngine] Task manager error:', errorMsg); + this.syncStatus = 'error'; + this.emit('syncFailed', errorMsg); + return { + success: false, + pushed: 0, + pulled: 0, + conflicts: 0, + errors: [errorMsg], + }; + } } } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 74f20b9..22daaab 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -46,19 +46,11 @@ const App: React.FC = () => { setMedia(media as MediaData[]); } - // Re-configure sync backends from saved credentials + // Re-configure Dropbox sync from saved credentials const savedCreds = localStorage.getItem('bds-credentials'); if (savedCreds) { try { const creds = JSON.parse(savedCreds); - if (creds.tursoUrl && creds.tursoToken) { - await window.electronAPI?.sync.configure({ - tursoUrl: creds.tursoUrl, - tursoAuthToken: creds.tursoToken, - autoSync: true, - syncInterval: 5, - }); - } if (creds.dropboxAccessToken && creds.dropboxAppKey) { await window.electronAPI?.dropbox?.configure({ accessToken: creds.dropboxAccessToken, @@ -71,7 +63,7 @@ const App: React.FC = () => { } } - // Check sync status + // Check sync status (uses Dropbox configuration) const syncConfigured = await window.electronAPI?.sync.isConfigured(); setSyncConfigured(syncConfigured || false); diff --git a/src/renderer/components/CredentialsPanel/CredentialsPanel.tsx b/src/renderer/components/CredentialsPanel/CredentialsPanel.tsx index 6af55b9..9e02590 100644 --- a/src/renderer/components/CredentialsPanel/CredentialsPanel.tsx +++ b/src/renderer/components/CredentialsPanel/CredentialsPanel.tsx @@ -3,8 +3,6 @@ import { showToast } from '../Toast'; import './CredentialsPanel.css'; interface Credentials { - tursoUrl: string; - tursoToken: string; ftpHost?: string; ftpUser?: string; ftpPassword?: string; @@ -15,8 +13,6 @@ interface Credentials { export const CredentialsPanel: React.FC = () => { const [credentials, setCredentials] = useState({ - tursoUrl: '', - tursoToken: '', ftpHost: '', ftpUser: '', ftpPassword: '', @@ -24,7 +20,7 @@ export const CredentialsPanel: React.FC = () => { sshUser: '', sshKeyPath: '', }); - const [activeTab, setActiveTab] = useState<'sync' | 'ftp' | 'ssh'>('sync'); + const [activeTab, setActiveTab] = useState<'ftp' | 'ssh'>('ftp'); const [showTokens, setShowTokens] = useState(false); // Load saved credentials (in a real app, use secure storage) @@ -47,16 +43,6 @@ export const CredentialsPanel: React.FC = () => { // Save to localStorage (in production, use secure storage) localStorage.setItem('bds-credentials', JSON.stringify(credentials)); - // Configure sync if Turso credentials are set - if (credentials.tursoUrl && credentials.tursoToken) { - await window.electronAPI?.sync.configure({ - tursoUrl: credentials.tursoUrl, - tursoAuthToken: credentials.tursoToken, - autoSync: true, - syncInterval: 5, - }); - } - showToast.success('Credentials saved'); } catch (error) { console.error('Failed to save credentials:', error); @@ -64,13 +50,9 @@ export const CredentialsPanel: React.FC = () => { } }; - const handleClear = (type: 'sync' | 'ftp' | 'ssh') => { + const handleClear = (type: 'ftp' | 'ssh') => { const newCreds = { ...credentials }; switch (type) { - case 'sync': - newCreds.tursoUrl = ''; - newCreds.tursoToken = ''; - break; case 'ftp': newCreds.ftpHost = ''; newCreds.ftpUser = ''; @@ -85,31 +67,20 @@ export const CredentialsPanel: React.FC = () => { setCredentials(newCreds); }; - const handleTestConnection = async (type: 'sync' | 'ftp' | 'ssh') => { + const handleTestConnection = async (type: 'ftp' | 'ssh') => { showToast.loading(`Testing ${type.toUpperCase()} connection...`); // Simulate connection test await new Promise(resolve => setTimeout(resolve, 1500)); // In a real implementation, this would test the actual connection - if (type === 'sync' && credentials.tursoUrl && credentials.tursoToken) { - showToast.dismiss(); - showToast.success('Sync connection successful'); - } else { - showToast.dismiss(); - showToast.error('Connection failed - check credentials'); - } + showToast.dismiss(); + showToast.error('Connection failed - check credentials'); }; return (
-
- {activeTab === 'sync' && ( -
-
-

Turso/LibSQL Cloud Sync

-

- Connect to Turso for cloud database synchronization. -

-
- -
- - setCredentials({ ...credentials, tursoUrl: e.target.value })} - /> -
- -
- - setCredentials({ ...credentials, tursoToken: e.target.value })} - /> -
- -
- - - -
-
- )} - {activeTab === 'ftp' && (
diff --git a/src/renderer/components/SettingsView/SettingsView.tsx b/src/renderer/components/SettingsView/SettingsView.tsx index 117d860..81d60d4 100644 --- a/src/renderer/components/SettingsView/SettingsView.tsx +++ b/src/renderer/components/SettingsView/SettingsView.tsx @@ -17,9 +17,6 @@ export const scrollToSettingsSection = (category: SettingsCategory) => { // Settings categories interface Credentials { - // Turso Cloud Sync - tursoUrl: string; - tursoToken: string; // Dropbox File Sync dropboxAccessToken: string; dropboxAppKey: string; @@ -35,8 +32,6 @@ interface Credentials { } const defaultCredentials: Credentials = { - tursoUrl: '', - tursoToken: '', dropboxAccessToken: '', dropboxAppKey: '', dropboxRemotePath: '/blog', @@ -173,29 +168,6 @@ export const SettingsView: React.FC = () => { loadSettings(); }, []); - // Save credentials and configure backends - const handleSaveTurso = async () => { - try { - localStorage.setItem('bds-credentials', JSON.stringify(credentials)); - - if (credentials.tursoUrl && credentials.tursoToken) { - await window.electronAPI?.sync.configure({ - tursoUrl: credentials.tursoUrl, - tursoAuthToken: credentials.tursoToken, - autoSync: true, - syncInterval: 5, - }); - useAppStore.getState().setSyncConfigured(true); - showToast.success('Cloud sync configured'); - } else { - showToast.success('Credentials saved'); - } - } catch (error) { - console.error('Failed to save Turso credentials:', error); - showToast.error('Failed to configure cloud sync'); - } - }; - const handleSaveDropbox = async () => { try { localStorage.setItem('bds-credentials', JSON.stringify(credentials)); @@ -227,14 +199,9 @@ export const SettingsView: React.FC = () => { } }; - const handleClearCredentials = (type: 'turso' | 'dropbox' | 'ftp' | 'ssh') => { + const handleClearCredentials = (type: 'dropbox' | 'ftp' | 'ssh') => { const newCreds = { ...credentials }; switch (type) { - case 'turso': - newCreds.tursoUrl = ''; - newCreds.tursoToken = ''; - useAppStore.getState().setSyncConfigured(false); - break; case 'dropbox': newCreds.dropboxAccessToken = ''; newCreds.dropboxAppKey = ''; @@ -270,31 +237,19 @@ export const SettingsView: React.FC = () => { } }; - const handleTestConnection = async (type: 'turso' | 'dropbox') => { - showToast.loading(`Testing ${type} connection...`); + const handleTestDropboxConnection = async () => { + showToast.loading('Testing Dropbox connection...'); try { - if (type === 'turso') { - // Simulate connection test - await new Promise(resolve => setTimeout(resolve, 1500)); - if (credentials.tursoUrl && credentials.tursoToken) { - showToast.dismiss(); - showToast.success('Cloud sync connection successful'); - } else { - showToast.dismiss(); - showToast.error('Missing credentials'); - } + const status = await window.electronAPI?.dropbox?.getStatus(); + showToast.dismiss(); + if (status) { + showToast.success('Dropbox connection active'); } else { - const status = await window.electronAPI?.dropbox?.getStatus(); - showToast.dismiss(); - if (status) { - showToast.success('Dropbox connection active'); - } else { - showToast.error('Dropbox connection failed'); - } + showToast.error('Dropbox connection failed'); } } catch { showToast.dismiss(); - showToast.error(`${type} connection failed`); + showToast.error('Dropbox connection failed'); } }; @@ -321,7 +276,7 @@ export const SettingsView: React.FC = () => { const projectKeywords = ['project', 'name', 'description', 'blog', 'site']; const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual']; const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page']; - const syncKeywords = ['sync', 'turso', 'libsql', 'cloud', 'database', 'dropbox', 'file', 'backup', 'token', 'remote']; + const syncKeywords = ['sync', 'dropbox', 'file', 'backup', 'token', 'remote']; const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload']; const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem']; @@ -485,68 +440,6 @@ export const SettingsView: React.FC = () => { <> - -