diff --git a/CONFIG.md b/CONFIG.md new file mode 100644 index 0000000..6457cf1 --- /dev/null +++ b/CONFIG.md @@ -0,0 +1,225 @@ +# Configuration Management + +This document explains how to use the configuration management system in Commad. + +## Overview + +The configuration system provides: +- Global configuration store using React Context +- Persistent storage in localStorage +- Browser console tools for easy configuration management +- Validation for configuration values +- Real-time updates across the application + +## Available Configuration Options + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `couchdbUrl` | string | `/db` | URL for CouchDB server (use `/db` for Vite proxy) | +| `couchdbUsername` | string | `""` | CouchDB username (optional) | +| `couchdbPassword` | string | `""` | CouchDB password (optional) | +| `syncEnabled` | boolean | `false` | Enable/disable synchronization | +| `syncInterval` | number | `30000` | Sync interval in milliseconds | +| `appName` | string | `commad` | Application name | +| `theme` | string | `light` | UI theme preference | + +## Using Console Tools + +The configuration can be managed through the browser console using the `commad` global object. + +### Configuration Commands + +```javascript +// Show help +commad.help() +commad.config.help() + +// Quick setup (recommended for 401 errors) +commad.config.setup("http://localhost:5984", "username", "password") + +// Manual configuration +// Set CouchDB URL with validation +commad.config.setCouchDB('/db') // Use Vite proxy (recommended) +// OR +commad.config.setCouchDB('http://localhost:5984') // Direct connection +commad.config.setAuth('username', 'password') + +// View current configuration +commad.config.getAll() + +// Get a specific config value +commad.config.get('couchdbUrl') + +// Set a configuration value +commad.config.set('syncEnabled', true) + +// Clear authentication +commad.config.clearAuth() +``` + +### Sync Management Commands + +```javascript +// Show sync help +commad.sync.help() + +// Check sync status +commad.sync.status() +commad.sync.info() + +// Manual sync operations +commad.sync.force() // Force a full sync +commad.sync.push() // Push local changes to remote +commad.sync.pull() // Pull changes from remote + +// Connection management +commad.sync.start() // Start continuous sync +commad.sync.stop() // Stop sync +commad.sync.reconnect() // Reconnect to remote + +// Conflict resolution +commad.sync.conflicts() // List documents with conflicts +commad.sync.resolve(docId, winningRev, losingRevs) // Resolve conflict +``` + +### Advanced Commands + +```javascript +// Export configuration as JSON +commad.config.export() + +// Import configuration from JSON +commad.config.import('{"couchdbUrl": "http://example.com:5984", "syncEnabled": true}') + +// Reset to default configuration +commad.config.reset() + +// Test CouchDB connection +commad.utils.testCouchDB() + +// Clear all application data +commad.utils.clearData() +``` + +## Using in React Components + +```jsx +import { useConfig } from '../contexts/ConfigContext'; + +function MyComponent() { + const { config, setConfig, setCouchDBUrl } = useConfig(); + + // Access config values + const couchdbUrl = config.couchdbUrl; + + // Update configuration + const handleUrlChange = (url) => { + setCouchDBUrl(url); + }; + + return ( +
+

Current CouchDB URL: {couchdbUrl}

+ +
+ ); +} +``` + +## Using ConfigService Directly + +```javascript +import configManager from '../services/ConfigService'; + +// Get configuration +const url = configManager.get('couchdbUrl'); + +// Set configuration +configManager.set('syncEnabled', true); + +// Listen for changes +configManager.addListener((newConfig) => { + console.log('Config changed:', newConfig); +}); +``` + +## Proxy Setup (Recommended) + +The application uses Vite's built-in proxy to avoid CORS issues during development. The proxy is configured in `vite.config.js`: + +```javascript +server: { + proxy: { + '/db': { + target: process.env.COUCHDB_URL || 'http://localhost:5984', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/db/, '') + } + } +} +``` + +### Using the Proxy + +1. **Default setup**: The application is pre-configured to use `/db` as the CouchDB URL +2. **Custom CouchDB target**: Set the `COUCHDB_URL` environment variable +3. **No CORS configuration needed**: The proxy handles all cross-origin requests + +### Environment Variables + +Create a `.env` file in the project root: + +```bash +# Optional: Custom CouchDB URL for proxy target +COUCHDB_URL=http://localhost:5984 +# or for GitHub Codespaces +COUCHDB_URL=https://your-codespace-5984.app.github.dev +``` + +## Getting Started + +1. Open the browser developer console +2. Type `commad.help()` to see available commands +3. **Quick setup with proxy** (recommended): `commad.config.setup("/db", "username", "password")` +4. **Or set URL manually**: `commad.config.setCouchDB("/db")` (for proxy) or `commad.config.setCouchDB("http://your-couchdb-url:5984")` (direct) +5. **If you get a 401 error**: Set authentication: `commad.config.setAuth("username", "password")` +6. Enable sync: `commad.sync.start()` +7. Check sync status: `commad.sync.status()` +8. View the configuration and sync panels in the UI + +### Troubleshooting 401 Errors + +If you encounter 401 Unauthorized errors: + +1. **Check if CouchDB requires authentication**: + ```javascript + commad.utils.testCouchDB() + ``` + +2. **Set your credentials**: + ```javascript + commad.config.setAuth("your-username", "your-password") + ``` + +3. **Test the connection again**: + ```javascript + commad.utils.testCouchDB() + ``` + +4. **Enable sync**: + ```javascript + commad.sync.start() + ``` + +The configuration is automatically saved to localStorage and will persist between browser sessions. Sync runs continuously in the background when enabled. + +## Sync Features + +- **Continuous sync**: Real-time bidirectional synchronization with CouchDB +- **Offline support**: Works offline and syncs when connection is restored +- **Conflict resolution**: Automatic conflict detection with manual resolution tools +- **Manual sync**: Force sync, push-only, or pull-only operations +- **Status monitoring**: Real-time sync status in the UI and console +- **Error handling**: Comprehensive error reporting and recovery diff --git a/package.json b/package.json index fe66609..ddd7d8d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "homepage": "https://sofadb.github.io/commad", "scripts": { "dev": "vite", + "proxy": "node proxy-server.js", + "dev:proxy": "concurrently \"npm run proxy\" \"npm run dev\"", "build": "vite build", "lint": "eslint .", "preview": "vite preview", @@ -36,11 +38,15 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "concurrently": "^8.2.2", + "cors": "^2.8.5", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", + "express": "^4.18.2", "gh-pages": "^6.3.0", "globals": "^16.3.0", + "http-proxy-middleware": "^2.0.6", "vite": "^7.0.4" } } diff --git a/proxy-server.js b/proxy-server.js new file mode 100644 index 0000000..71aa1d0 --- /dev/null +++ b/proxy-server.js @@ -0,0 +1,85 @@ +/** + * Development proxy server for CouchDB + * This proxy helps avoid CORS issues during development + */ +import { createProxyMiddleware } from 'http-proxy-middleware'; +import express from 'express'; +import cors from 'cors'; + +const app = express(); +const PORT = process.env.PROXY_PORT || 3001; +const COUCHDB_URL = process.env.COUCHDB_URL || 'http://localhost:5984'; + +// Enable CORS for all routes +app.use(cors({ + origin: true, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With'] +})); + +// Proxy middleware configuration +const proxyOptions = { + target: COUCHDB_URL, + changeOrigin: true, + logLevel: 'debug', + onProxyReq: (proxyReq, req, res) => { + console.log(`[PROXY] ${req.method} ${req.url} -> ${COUCHDB_URL}${req.url}`); + }, + onProxyRes: (proxyRes, req, res) => { + // Ensure CORS headers are set + proxyRes.headers['Access-Control-Allow-Origin'] = req.headers.origin || '*'; + proxyRes.headers['Access-Control-Allow-Credentials'] = 'true'; + proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, HEAD, OPTIONS'; + proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Accept, Origin, X-Requested-With'; + }, + onError: (err, req, res) => { + console.error('[PROXY ERROR]', err.message); + res.status(500).json({ + error: 'Proxy Error', + message: err.message, + target: COUCHDB_URL + }); + } +}; + +// Create proxy middleware +const proxy = createProxyMiddleware(proxyOptions); + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ + status: 'ok', + proxy: 'CouchDB Development Proxy', + target: COUCHDB_URL, + timestamp: new Date().toISOString() + }); +}); + +// Proxy all other requests to CouchDB +app.use('/', proxy); + +// Start the proxy server +app.listen(PORT, () => { + console.log(` +🚀 CouchDB Development Proxy Server +=================================== +Proxy URL: http://localhost:${PORT} +Target: ${COUCHDB_URL} +Health Check: http://localhost:${PORT}/health + +Use this proxy URL in your application configuration: +commad.config.setCouchDB("http://localhost:${PORT}") + `); +}); + +// Handle graceful shutdown +process.on('SIGTERM', () => { + console.log('Shutting down proxy server...'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('Shutting down proxy server...'); + process.exit(0); +}); diff --git a/src/App.jsx b/src/App.jsx index 0add1fe..f75683d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,9 @@ -import { useState } from 'react' -import './App.css' -import CodeEditor from './components/CodeEditor' +import React, { useState } from 'react'; +import './App.css'; +import CodeEditor from './components/CodeEditor'; +import CommandPalette from './components/CommandPalette'; +import ConfigDisplay from './components/ConfigDisplay'; +import SyncStatus from './components/SyncStatus'; function App() { const [code, setCode] = useState(`# Markdown Editor @@ -69,6 +72,8 @@ Just pure markdown return (
+ + { + const { config } = useConfig(); + + return ( +
+ {showTitle &&

Current Configuration

} +
+
+ + {config.couchdbUrl} +
+
+ + + {config.syncEnabled ? 'Yes' : 'No'} + +
+
+ + {config.syncInterval / 1000}s +
+
+ + {config.theme} +
+
+
+

💡 Use browser console commands to modify configuration:

+ commad.config.setCouchDB("http://your-couchdb-url:5984") +
+
+ ); +}; + +export default ConfigDisplay; diff --git a/src/components/SyncStatus.css b/src/components/SyncStatus.css new file mode 100644 index 0000000..0e39185 --- /dev/null +++ b/src/components/SyncStatus.css @@ -0,0 +1,216 @@ +.sync-status { + border: 1px solid #e0e0e0; + border-radius: 8px; + margin: 1rem 0; + background: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.sync-status-header { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.2s; + border-radius: 8px 8px 0 0; +} + +.sync-status-header:hover { + background-color: #f5f5f5; +} + +.sync-icon { + font-size: 1.2rem; + margin-right: 0.5rem; +} + +.sync-text { + flex: 1; + font-weight: 500; + color: #333; +} + +.conflict-badge { + background: #ff4444; + color: white; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: bold; + margin-right: 0.5rem; +} + +.toggle-icon { + color: #666; + font-size: 0.8rem; + transition: transform 0.2s; +} + +.sync-details { + border-top: 1px solid #e0e0e0; + padding: 1rem; + background: #fafafa; + border-radius: 0 0 8px 8px; +} + +.sync-info { + margin-bottom: 1rem; +} + +.info-row { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; + padding: 0.25rem 0; +} + +.info-row:last-child { + margin-bottom: 0; +} + +.label { + font-weight: 500; + color: #555; + min-width: 100px; +} + +.value { + color: #333; + text-align: right; + flex: 1; +} + +.value.url { + font-family: monospace; + font-size: 0.9rem; + word-break: break-all; +} + +.info-row.error .value { + color: #d32f2f; +} + +.sync-actions { + margin-bottom: 1rem; +} + +.sync-button { + background: #1976d2; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s; +} + +.sync-button:hover:not(:disabled) { + background: #1565c0; +} + +.sync-button:disabled { + background: #ccc; + cursor: not-allowed; +} + +.conflicts-section { + border-top: 1px solid #ddd; + padding-top: 1rem; +} + +.conflicts-section h4 { + margin: 0 0 0.5rem 0; + color: #d32f2f; + font-size: 1rem; +} + +.conflicts-list { + margin-bottom: 0.5rem; +} + +.conflict-item { + display: flex; + justify-content: space-between; + padding: 0.25rem 0.5rem; + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 4px; + margin-bottom: 0.25rem; +} + +.conflict-id { + font-family: monospace; + font-size: 0.9rem; + color: #856404; +} + +.conflict-count { + font-size: 0.8rem; + color: #856404; +} + +.conflict-note { + font-size: 0.8rem; + color: #666; + margin: 0.5rem 0 0 0; + font-style: italic; +} + +.conflict-note code { + background: #f0f0f0; + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-size: 0.75rem; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .sync-status { + background: #2d2d2d; + border-color: #444; + } + + .sync-status-header { + color: #fff; + } + + .sync-status-header:hover { + background-color: #3d3d3d; + } + + .sync-text { + color: #fff; + } + + .sync-details { + background: #1e1e1e; + border-color: #444; + } + + .label { + color: #ccc; + } + + .value { + color: #fff; + } + + .conflicts-section { + border-color: #555; + } + + .conflict-item { + background: #3d3416; + border-color: #6d5d00; + } + + .conflict-note code { + background: #444; + color: #fff; + } +} diff --git a/src/components/SyncStatus.jsx b/src/components/SyncStatus.jsx new file mode 100644 index 0000000..205b277 --- /dev/null +++ b/src/components/SyncStatus.jsx @@ -0,0 +1,182 @@ +import React, { useState, useEffect } from 'react'; +import './SyncStatus.css'; +import { DatabaseService } from '../services/DatabaseService'; + +const SyncStatus = () => { + const [syncStatus, setSyncStatus] = useState({ + status: 'disconnected', + isOnline: navigator.onLine, + lastSyncTime: null, + error: null, + isConnected: false, + couchdbUrl: null + }); + + const [conflicts, setConflicts] = useState([]); + const [showDetails, setShowDetails] = useState(false); + + useEffect(() => { + // Get initial status + const status = DatabaseService.getSyncStatus(); + setSyncStatus(status); + + // Listen for sync status changes + const unsubscribe = DatabaseService.addSyncListener((newStatus) => { + setSyncStatus(newStatus); + }); + + // Check for conflicts periodically + const checkConflicts = async () => { + try { + const conflictList = await DatabaseService.getConflicts(); + setConflicts(conflictList); + } catch (error) { + console.error('Error checking conflicts:', error); + } + }; + + checkConflicts(); + const conflictInterval = setInterval(checkConflicts, 30000); // Check every 30 seconds + + return () => { + unsubscribe(); + clearInterval(conflictInterval); + }; + }, []); + + const getStatusIcon = () => { + if (!syncStatus.isOnline) return '🔴'; + + switch (syncStatus.status) { + case 'connected': + case 'up-to-date': + return 'đŸŸĸ'; + case 'syncing': + return '🟡'; + case 'error': + return '🔴'; + case 'offline': + return 'âšĢ'; + default: + return 'âšĒ'; + } + }; + + const getStatusText = () => { + if (!syncStatus.isOnline) return 'Offline'; + + switch (syncStatus.status) { + case 'connected': + return 'Connected'; + case 'up-to-date': + return 'Up to date'; + case 'syncing': + return 'Syncing...'; + case 'error': + return 'Error'; + case 'offline': + return 'Offline'; + case 'disconnected': + return 'Disconnected'; + default: + return 'Unknown'; + } + }; + + const handleForceSync = async () => { + try { + await DatabaseService.forceSync(); + } catch (error) { + console.error('Manual sync failed:', error); + } + }; + + const formatTime = (timeString) => { + if (!timeString) return 'Never'; + return new Date(timeString).toLocaleString(); + }; + + return ( +
+
setShowDetails(!showDetails)}> + {getStatusIcon()} + {getStatusText()} + {conflicts.length > 0 && ( + {conflicts.length} + )} + {showDetails ? 'â–ŧ' : 'â–ļ'} +
+ + {showDetails && ( +
+
+
+ Status: + {getStatusText()} +
+ +
+ Online: + {syncStatus.isOnline ? 'Yes' : 'No'} +
+ +
+ Connected: + {syncStatus.isConnected ? 'Yes' : 'No'} +
+ +
+ Last Sync: + {formatTime(syncStatus.lastSyncTime)} +
+ + {syncStatus.couchdbUrl && ( +
+ CouchDB: + {syncStatus.couchdbUrl} +
+ )} + + {syncStatus.error && ( +
+ Error: + {syncStatus.error} +
+ )} +
+ +
+ +
+ + {conflicts.length > 0 && ( +
+

Conflicts ({conflicts.length})

+
+ {conflicts.map(conflict => ( +
+ {conflict.id} + + {conflict.conflicts.length} revision(s) + +
+ ))} +
+

+ Use console tools to resolve conflicts: commad.sync.conflicts() +

+
+ )} +
+ )} +
+ ); +}; + +export default SyncStatus; diff --git a/src/contexts/ConfigContext.jsx b/src/contexts/ConfigContext.jsx new file mode 100644 index 0000000..beaf352 --- /dev/null +++ b/src/contexts/ConfigContext.jsx @@ -0,0 +1,60 @@ +/** + * ConfigContext - React context for configuration management + */ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import configManager from '../services/ConfigService'; + +// Create the context +const ConfigContext = createContext(); + +/** + * ConfigProvider component to wrap the app and provide config context + */ +export const ConfigProvider = ({ children }) => { + const [config, setConfig] = useState(configManager.getAll()); + + useEffect(() => { + // Add listener for config changes + const handleConfigChange = (newConfig) => { + setConfig({ ...newConfig }); + }; + + configManager.addListener(handleConfigChange); + + // Cleanup listener on unmount + return () => { + configManager.removeListener(handleConfigChange); + }; + }, []); + + const contextValue = { + config, + getConfig: configManager.get.bind(configManager), + setConfig: configManager.set.bind(configManager), + setMultipleConfig: configManager.setMultiple.bind(configManager), + resetConfig: configManager.reset.bind(configManager), + setCouchDBUrl: configManager.setCouchDBUrl.bind(configManager), + validateCouchDBUrl: configManager.validateCouchDBUrl.bind(configManager), + exportConfig: configManager.export.bind(configManager), + importConfig: configManager.import.bind(configManager) + }; + + return ( + + {children} + + ); +}; + +/** + * Custom hook to use the config context + */ +export const useConfig = () => { + const context = useContext(ConfigContext); + if (!context) { + throw new Error('useConfig must be used within a ConfigProvider'); + } + return context; +}; + +export default ConfigContext; diff --git a/src/hooks/useConfigValue.js b/src/hooks/useConfigValue.js new file mode 100644 index 0000000..1f50ace --- /dev/null +++ b/src/hooks/useConfigValue.js @@ -0,0 +1,51 @@ +/** + * useConfigValue - Custom hook for accessing specific config values + */ +import { useConfig } from '../contexts/ConfigContext'; + +/** + * Hook to get and set a specific configuration value + * @param {string} key - Configuration key + * @returns {[value, setValue]} - Current value and setter function + */ +export const useConfigValue = (key) => { + const { config, setConfig } = useConfig(); + + const value = config[key]; + const setValue = (newValue) => setConfig(key, newValue); + + return [value, setValue]; +}; + +/** + * Hook specifically for CouchDB URL management + * @returns {[url, setUrl, isValid]} - URL, setter, and validation status + */ +export const useCouchDBUrl = () => { + const { config, setCouchDBUrl, validateCouchDBUrl } = useConfig(); + + const url = config.couchdbUrl; + const isValid = validateCouchDBUrl(url); + + return [url, setCouchDBUrl, isValid]; +}; + +/** + * Hook for sync configuration + * @returns {[syncConfig, setSyncEnabled, setSyncInterval]} - Sync settings and setters + */ +export const useSyncConfig = () => { + const { config, setConfig } = useConfig(); + + const syncConfig = { + enabled: config.syncEnabled, + interval: config.syncInterval + }; + + const setSyncEnabled = (enabled) => setConfig('syncEnabled', enabled); + const setSyncInterval = (interval) => setConfig('syncInterval', interval); + + return [syncConfig, setSyncEnabled, setSyncInterval]; +}; + +export default useConfigValue; diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..a28bfe6 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,9 +2,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import { ConfigProvider } from './contexts/ConfigContext.jsx' +import { exposeConsoleTools } from './utils/consoleTools.js' + +// Initialize console tools for development +exposeConsoleTools(); createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/src/services/ConfigService.js b/src/services/ConfigService.js new file mode 100644 index 0000000..7b339e1 --- /dev/null +++ b/src/services/ConfigService.js @@ -0,0 +1,209 @@ +/** + * ConfigService - Service for managing application configuration + */ + +// Default configuration values +const DEFAULT_CONFIG = { + couchdbUrl: '/db', // Use Vite proxy path for development + couchdbUsername: '', + couchdbPassword: '', + syncEnabled: false, + syncInterval: 30000, // 30 seconds + appName: 'commad', + theme: 'light' +}; + +// Configuration storage key +const CONFIG_STORAGE_KEY = 'commad-config'; + +class ConfigManager { + constructor() { + this.config = this.loadConfig(); + this.listeners = new Set(); + } + + /** + * Load configuration from localStorage or use defaults + * @returns {Object} Configuration object + */ + loadConfig() { + try { + const storedConfig = localStorage.getItem(CONFIG_STORAGE_KEY); + if (storedConfig) { + return { ...DEFAULT_CONFIG, ...JSON.parse(storedConfig) }; + } + } catch (error) { + console.warn('Error loading config from localStorage:', error); + } + return { ...DEFAULT_CONFIG }; + } + + /** + * Save configuration to localStorage + */ + saveConfig() { + try { + localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(this.config)); + this.notifyListeners(); + } catch (error) { + console.error('Error saving config to localStorage:', error); + } + } + + /** + * Get a configuration value + * @param {string} key - Configuration key + * @returns {*} Configuration value + */ + get(key) { + return this.config[key]; + } + + /** + * Set a configuration value + * @param {string} key - Configuration key + * @param {*} value - Configuration value + */ + set(key, value) { + const oldValue = this.config[key]; + this.config[key] = value; + + console.log(`Config updated: ${key} = ${JSON.stringify(value)} (was: ${JSON.stringify(oldValue)})`); + + this.saveConfig(); + } + + /** + * Set multiple configuration values + * @param {Object} updates - Object containing key-value pairs to update + */ + setMultiple(updates) { + const changes = {}; + + Object.entries(updates).forEach(([key, value]) => { + const oldValue = this.config[key]; + this.config[key] = value; + changes[key] = { old: oldValue, new: value }; + }); + + console.log('Config updated:', changes); + + this.saveConfig(); + } + + /** + * Get all configuration + * @returns {Object} Complete configuration object + */ + getAll() { + return { ...this.config }; + } + + /** + * Reset configuration to defaults + */ + reset() { + this.config = { ...DEFAULT_CONFIG }; + console.log('Config reset to defaults'); + this.saveConfig(); + } + + /** + * Export configuration as JSON string + * @returns {string} JSON string of configuration + */ + export() { + return JSON.stringify(this.config, null, 2); + } + + /** + * Import configuration from JSON string + * @param {string} jsonString - JSON string containing configuration + */ + import(jsonString) { + try { + const importedConfig = JSON.parse(jsonString); + this.config = { ...DEFAULT_CONFIG, ...importedConfig }; + console.log('Config imported successfully'); + this.saveConfig(); + } catch (error) { + console.error('Error importing config:', error); + throw new Error('Invalid JSON configuration'); + } + } + + /** + * Add a listener for configuration changes + * @param {Function} listener - Callback function to call when config changes + */ + addListener(listener) { + this.listeners.add(listener); + } + + /** + * Remove a configuration change listener + * @param {Function} listener - Listener function to remove + */ + removeListener(listener) { + this.listeners.delete(listener); + } + + /** + * Notify all listeners of configuration changes + */ + notifyListeners() { + this.listeners.forEach(listener => { + try { + listener(this.config); + } catch (error) { + console.error('Error calling config listener:', error); + } + }); + } + + /** + * Validate CouchDB URL format + * @param {string} url - URL to validate + * @returns {boolean} True if valid, false otherwise + */ + validateCouchDBUrl(url) { + // Allow relative paths for proxy (like '/db') + if (url.startsWith('/')) { + return true; + } + + try { + const parsed = new URL(url); + return ['http:', 'https:'].includes(parsed.protocol); + } catch { + return false; + } + } + + /** + * Set CouchDB URL with validation + * @param {string} url - CouchDB URL + */ + setCouchDBUrl(url) { + if (!this.validateCouchDBUrl(url)) { + throw new Error('Invalid CouchDB URL format. Must be a valid HTTP or HTTPS URL.'); + } + this.set('couchdbUrl', url); + } +} + +// Create a singleton instance +const configManager = new ConfigManager(); + +// Export the singleton instance +export default configManager; + +// Export individual methods for convenience +export const { + get: getConfig, + set: setConfig, + getAll: getAllConfig, + reset: resetConfig, + setCouchDBUrl, + validateCouchDBUrl +} = configManager; diff --git a/src/services/DatabaseService.js b/src/services/DatabaseService.js index 863527f..d083189 100644 --- a/src/services/DatabaseService.js +++ b/src/services/DatabaseService.js @@ -2,6 +2,7 @@ * DatabaseService - Service for interacting with PouchDB */ import PouchDB from 'pouchdb'; +import syncService from './SyncService.js'; // Create a database instance const db = new PouchDB('commad-documents'); @@ -128,5 +129,49 @@ export const DatabaseService = { console.error(`Error deleting document with id ${id}:`, error); return false; } + }, + + /** + * Get sync status + * @returns {Object} Current sync status + */ + getSyncStatus: () => { + return syncService.getStatus(); + }, + + /** + * Force a sync with remote database + * @returns {Promise} Promise resolving when sync is complete + */ + forceSync: async () => { + return await syncService.forceSync(); + }, + + /** + * Add listener for sync status changes + * @param {Function} callback - Callback function to call on status changes + * @returns {Function} Unsubscribe function + */ + addSyncListener: (callback) => { + return syncService.addListener(callback); + }, + + /** + * Get conflicts that need resolution + * @returns {Promise} Promise resolving to array of conflict objects + */ + getConflicts: async () => { + return await syncService.getConflicts(); + }, + + /** + * Resolve a document conflict + * @param {string} docId - Document ID + * @param {string} winningRev - Revision to keep + * @param {Array} losingRevs - Revisions to remove + * @returns {Promise} Promise resolving to success status + */ + resolveConflict: async (docId, winningRev, losingRevs) => { + return await syncService.resolveConflict(docId, winningRev, losingRevs); } }; diff --git a/src/services/SyncService.js b/src/services/SyncService.js new file mode 100644 index 0000000..7f5dcf7 --- /dev/null +++ b/src/services/SyncService.js @@ -0,0 +1,390 @@ +/** + * SyncService - Service for syncing with CouchDB + */ +import PouchDB from 'pouchdb'; +import configManager from './ConfigService.js'; + +class SyncService { + constructor() { + this.localDB = new PouchDB('commad-documents'); + this.remoteDB = null; + this.syncHandler = null; + this.isOnline = navigator.onLine; + this.syncStatus = 'disconnected'; + this.lastSyncTime = null; + this.syncError = null; + this.listeners = new Set(); + + // Listen for config changes + configManager.addListener(this.handleConfigChange.bind(this)); + + // Listen for online/offline events + window.addEventListener('online', this.handleOnline.bind(this)); + window.addEventListener('offline', this.handleOffline.bind(this)); + + // Initialize sync if enabled + this.initializeSync(); + } + + /** + * Initialize sync based on current configuration + */ + async initializeSync() { + const config = configManager.getAll(); + if (config.syncEnabled && config.couchdbUrl) { + await this.setupSync(config.couchdbUrl); + } + } + + /** + * Handle configuration changes + */ + handleConfigChange(newConfig) { + if (newConfig.syncEnabled && newConfig.couchdbUrl) { + this.setupSync(newConfig.couchdbUrl); + } else { + this.stopSync(); + } + } + + /** + * Setup sync with remote CouchDB + */ + async setupSync(couchdbUrl) { + try { + // Stop existing sync + this.stopSync(); + + // Get authentication from config + const config = configManager.getAll(); + const { couchdbUsername, couchdbPassword } = config; + + // Create remote database connection with auth if provided + let remoteUrl = `${couchdbUrl}`; + + if (couchdbUsername && couchdbPassword) { + // Add auth to URL + const url = new URL(couchdbUrl); + url.username = couchdbUsername; + url.password = couchdbPassword; + remoteUrl = `${url.toString()}`; + } + + this.remoteDB = new PouchDB(remoteUrl); + + // Test connection + const isConnected = await this.testConnection(); + if (!isConnected) { + throw new Error('Failed to connect to CouchDB - check URL and credentials'); + } + + // Start continuous sync + this.startContinuousSync(); + + this.syncStatus = 'connected'; + this.syncError = null; + this.notifyListeners(); + + console.log('Sync initialized with:', couchdbUrl); + } catch (error) { + console.error('Error setting up sync:', error); + this.syncStatus = 'error'; + this.syncError = error.message; + this.notifyListeners(); + } + } + + /** + * Test connection to remote database + */ + async testConnection() { + try { + if (!this.remoteDB) return false; + + await this.remoteDB.info(); + return true; + } catch (error) { + console.error('Connection test failed:', error); + return false; + } + } + + /** + * Start continuous sync + */ + startContinuousSync() { + if (!this.remoteDB || !this.isOnline) return; + + const config = configManager.getAll(); + + this.syncHandler = this.localDB.sync(this.remoteDB, { + live: true, + retry: true, + timeout: 30000, + heartbeat: config.syncInterval || 30000 + }); + + // Handle sync events + this.syncHandler + .on('change', (info) => { + console.log('Sync change:', info); + this.lastSyncTime = new Date().toISOString(); + this.syncStatus = 'syncing'; + this.notifyListeners(); + }) + .on('paused', (err) => { + if (err) { + console.error('Sync paused with error:', err); + this.syncStatus = 'error'; + this.syncError = err.message; + } else { + console.log('Sync paused (up to date)'); + this.syncStatus = 'up-to-date'; + this.syncError = null; + } + this.notifyListeners(); + }) + .on('active', () => { + console.log('Sync active'); + this.syncStatus = 'syncing'; + this.syncError = null; + this.notifyListeners(); + }) + .on('denied', (err) => { + console.error('Sync denied:', err); + this.syncStatus = 'error'; + this.syncError = 'Access denied'; + this.notifyListeners(); + }) + .on('complete', (info) => { + console.log('Sync complete:', info); + this.syncStatus = 'complete'; + this.lastSyncTime = new Date().toISOString(); + this.notifyListeners(); + }) + .on('error', (err) => { + console.error('Sync error:', err); + this.syncStatus = 'error'; + this.syncError = err.message; + this.notifyListeners(); + }); + } + + /** + * Stop sync + */ + stopSync() { + if (this.syncHandler) { + this.syncHandler.cancel(); + this.syncHandler = null; + } + + this.syncStatus = 'disconnected'; + this.syncError = null; + this.notifyListeners(); + + console.log('Sync stopped'); + } + + /** + * Force a one-time sync + */ + async forceSync() { + if (!this.remoteDB || !this.isOnline) { + throw new Error('Not connected to remote database'); + } + + try { + this.syncStatus = 'syncing'; + this.notifyListeners(); + + const result = await this.localDB.sync(this.remoteDB, { + timeout: 30000 + }); + + this.lastSyncTime = new Date().toISOString(); + this.syncStatus = 'up-to-date'; + this.syncError = null; + this.notifyListeners(); + + return result; + } catch (error) { + console.error('Force sync failed:', error); + this.syncStatus = 'error'; + this.syncError = error.message; + this.notifyListeners(); + throw error; + } + } + + /** + * Push local changes to remote + */ + async pushToRemote() { + if (!this.remoteDB || !this.isOnline) { + throw new Error('Not connected to remote database'); + } + + try { + this.syncStatus = 'syncing'; + this.notifyListeners(); + + const result = await this.localDB.replicate.to(this.remoteDB); + + this.lastSyncTime = new Date().toISOString(); + this.syncStatus = 'up-to-date'; + this.notifyListeners(); + + return result; + } catch (error) { + console.error('Push to remote failed:', error); + this.syncStatus = 'error'; + this.syncError = error.message; + this.notifyListeners(); + throw error; + } + } + + /** + * Pull changes from remote + */ + async pullFromRemote() { + if (!this.remoteDB || !this.isOnline) { + throw new Error('Not connected to remote database'); + } + + try { + this.syncStatus = 'syncing'; + this.notifyListeners(); + + const result = await this.localDB.replicate.from(this.remoteDB); + + this.lastSyncTime = new Date().toISOString(); + this.syncStatus = 'up-to-date'; + this.notifyListeners(); + + return result; + } catch (error) { + console.error('Pull from remote failed:', error); + this.syncStatus = 'error'; + this.syncError = error.message; + this.notifyListeners(); + throw error; + } + } + + /** + * Handle online event + */ + handleOnline() { + console.log('Network is back online'); + this.isOnline = true; + + const config = configManager.getAll(); + if (config.syncEnabled && config.couchdbUrl) { + this.startContinuousSync(); + } + } + + /** + * Handle offline event + */ + handleOffline() { + console.log('Network went offline'); + this.isOnline = false; + this.syncStatus = 'offline'; + this.notifyListeners(); + } + + /** + * Get sync status information + */ + getStatus() { + return { + status: this.syncStatus, + isOnline: this.isOnline, + lastSyncTime: this.lastSyncTime, + error: this.syncError, + isConnected: !!this.remoteDB, + couchdbUrl: this.remoteDB ? this.remoteDB.name : null + }; + } + + /** + * Add a listener for sync status changes + */ + addListener(callback) { + this.listeners.add(callback); + return () => this.listeners.delete(callback); + } + + /** + * Notify all listeners of status changes + */ + notifyListeners() { + const status = this.getStatus(); + this.listeners.forEach(callback => { + try { + callback(status); + } catch (error) { + console.error('Error in sync listener:', error); + } + }); + } + + /** + * Get conflict documents + */ + async getConflicts() { + try { + const allDocs = await this.localDB.allDocs({ + include_docs: true, + conflicts: true + }); + + return allDocs.rows + .filter(row => row.doc._conflicts) + .map(row => ({ + id: row.doc._id, + conflicts: row.doc._conflicts, + doc: row.doc + })); + } catch (error) { + console.error('Error getting conflicts:', error); + return []; + } + } + + /** + * Resolve a conflict by choosing a revision + */ + async resolveConflict(docId, winningRev, losingRevs) { + try { + // Remove losing revisions + for (const rev of losingRevs) { + await this.localDB.remove(docId, rev); + } + + console.log(`Conflict resolved for document ${docId}`); + return true; + } catch (error) { + console.error('Error resolving conflict:', error); + return false; + } + } + + /** + * Clean up resources + */ + destroy() { + this.stopSync(); + this.listeners.clear(); + + window.removeEventListener('online', this.handleOnline); + window.removeEventListener('offline', this.handleOffline); + } +} + +// Create and export singleton instance +const syncService = new SyncService(); +export default syncService; diff --git a/src/utils/consoleTools.js b/src/utils/consoleTools.js new file mode 100644 index 0000000..2a87a9f --- /dev/null +++ b/src/utils/consoleTools.js @@ -0,0 +1,586 @@ +/** + * Console Tools - Development utilities for configuration management + * These tools are exposed to the browser console for easy config management + */ +import configManager from '../services/ConfigService'; +import syncService from '../services/SyncService.js'; +import { DatabaseService } from '../services/DatabaseService.js'; + +// Console tools object that will be exposed globally +const consoleTools = { + // Configuration management + config: { + /** + * Get a configuration value + * @param {string} key - Configuration key + * @returns {*} Configuration value + */ + get: (key) => { + if (!key) { + console.log('Available config keys:', Object.keys(configManager.getAll())); + return configManager.getAll(); + } + const value = configManager.get(key); + console.log(`Config ${key}:`, value); + return value; + }, + + /** + * Set a configuration value + * @param {string} key - Configuration key + * @param {*} value - Configuration value + */ + set: (key, value) => { + if (!key) { + console.error('Please provide a configuration key'); + return; + } + configManager.set(key, value); + }, + + /** + * Set CouchDB URL with validation + * @param {string} url - CouchDB URL + */ + setCouchDB: (url) => { + if (!url) { + console.log('Usage: commad.config.setCouchDB("http://localhost:5984")'); + return; + } + + try { + // Validate URL format + new URL(url); + configManager.set('couchdbUrl', url); + console.log(`✅ CouchDB URL set to: ${url}`); + + // Test connection + consoleTools.utils.testCouchDB(); + } catch (error) { + console.error('❌ Invalid URL format:', error.message); + } + }, + + /** + * Set CouchDB authentication + * @param {string} username - Username + * @param {string} password - Password + */ + setAuth: (username, password) => { + if (!username || !password) { + console.log(` +Usage: commad.config.setAuth("username", "password") + +Note: Credentials are stored in localStorage. Only use this in development! +For production, consider using environment variables or secure credential storage. + `); + return; + } + + configManager.set('couchdbUsername', username); + configManager.set('couchdbPassword', password); + console.log('✅ CouchDB authentication credentials set'); + console.log('âš ī¸ Warning: Credentials stored in localStorage'); + + // Test connection with new credentials + consoleTools.utils.testCouchDB(); + }, + + /** + * Quick setup for authenticated CouchDB + * @param {string} url - CouchDB URL + * @param {string} username - Username + * @param {string} password - Password + */ + setup: (url, username, password) => { + if (!url) { + console.log(` +🚀 Quick Setup for CouchDB with Authentication +============================================= + +Usage: commad.config.setup(url, username, password) + +Example: + commad.config.setup("http://localhost:5984", "admin", "password") + +This will: +1. Set the CouchDB URL +2. Set authentication credentials +3. Test the connection +4. Enable sync if connection succeeds + `); + return; + } + + console.log('🚀 Setting up CouchDB connection...'); + + try { + // Set URL + new URL(url); // Validate URL + configManager.set('couchdbUrl', url); + console.log(`✅ CouchDB URL: ${url}`); + + // Set auth if provided + if (username && password) { + configManager.set('couchdbUsername', username); + configManager.set('couchdbPassword', password); + console.log(`✅ Authentication: ${username}`); + console.log('âš ī¸ Credentials stored in localStorage'); + } + + // Test connection + console.log('🔍 Testing connection...'); + consoleTools.utils.testCouchDB().then(() => { + // Enable sync if connection test passes + setTimeout(() => { + console.log('🔄 Enabling sync...'); + configManager.set('syncEnabled', true); + console.log('✅ Setup complete! Check the sync status in the UI.'); + }, 1000); + }); + + } catch (error) { + console.error('❌ Setup failed:', error.message); + } + }, + + /** + * Clear CouchDB authentication + */ + clearAuth: () => { + configManager.set('couchdbUsername', ''); + configManager.set('couchdbPassword', ''); + console.log('✅ CouchDB authentication credentials cleared'); + }, + + /** + * Get all configuration + */ + getAll: () => { + const config = configManager.getAll(); + console.table(config); + return config; + }, + + /** + * Reset configuration to defaults + */ + reset: () => { + if (confirm('Are you sure you want to reset all configuration to defaults?')) { + configManager.reset(); + console.log('✅ Configuration reset to defaults'); + } + }, + + /** + * Export configuration as JSON + */ + export: () => { + const exported = configManager.export(); + console.log('Configuration exported:\n', exported); + return exported; + }, + + /** + * Import configuration from JSON + * @param {string} jsonString - JSON configuration string + */ + import: (jsonString) => { + if (!jsonString) { + console.log('Usage: commad.config.import(\'{"couchdbUrl": "http://example.com"}\')'); + return; + } + try { + configManager.import(jsonString); + console.log('✅ Configuration imported successfully'); + } catch (error) { + console.error('❌ Error importing configuration:', error.message); + } + }, + + /** + * Show help for configuration commands + */ + help: () => { + console.log(` +🔧 Configuration Management Help +=============================== + +Available commands: +â€ĸ commad.config.get() - Show all config or get(key) for specific value +â€ĸ commad.config.set(key, value) - Set a configuration value +â€ĸ commad.config.setup(url, user, pass) - Quick setup with authentication +â€ĸ commad.config.setCouchDB(url) - Set CouchDB URL with validation +â€ĸ commad.config.setAuth(user, pass) - Set CouchDB authentication +â€ĸ commad.config.clearAuth() - Clear CouchDB credentials +â€ĸ commad.config.getAll() - Display all config in a table +â€ĸ commad.config.reset() - Reset to default configuration +â€ĸ commad.config.export() - Export config as JSON string +â€ĸ commad.config.import(json) - Import config from JSON string +â€ĸ commad.config.help() - Show this help + +Examples: +--------- +// Using Vite proxy (recommended for development) +commad.config.setup("/db", "admin", "password") + +// Using direct CouchDB URL +commad.config.setup("http://localhost:5984", "admin", "password") +commad.config.setCouchDB("http://localhost:5984") +commad.config.setAuth("myuser", "mypassword") +commad.config.set("syncEnabled", true) +commad.config.get("couchdbUrl") +commad.config.getAll() + +For 401 Authentication errors: +commad.config.setAuth("username", "password") +Or use the quick setup: +commad.config.setup("/db", "username", "password") + +Available config keys: +â€ĸ couchdbUrl - CouchDB server URL (use "/db" for Vite proxy) +â€ĸ couchdbUsername - CouchDB username (optional) +â€ĸ couchdbPassword - CouchDB password (optional) +â€ĸ syncEnabled - Enable/disable synchronization +â€ĸ syncInterval - Sync interval in milliseconds +â€ĸ appName - Application name +â€ĸ theme - UI theme preference + `); + } + }, + + // Utility functions + utils: { + /** + * Clear all application data + */ + clearData: () => { + if (confirm('Are you sure you want to clear ALL application data? This cannot be undone.')) { + localStorage.clear(); + indexedDB.deleteDatabase('commad-documents'); + location.reload(); + } + }, + + /** + * Show application info + */ + info: () => { + console.log(` +📱 Commad Application Info +========================= +Version: ${process.env.NODE_ENV === 'development' ? 'Development' : 'Production'} +Storage: LocalStorage + PouchDB +Config: ${Object.keys(configManager.getAll()).length} keys configured + `); + }, + + /** + * Test CouchDB connection + */ + testCouchDB: async () => { + const config = configManager.getAll(); + const { couchdbUrl, couchdbUsername, couchdbPassword } = config; + + console.log(`Testing connection to: ${couchdbUrl}`); + + try { + const headers = {}; + + // Add authentication if provided + if (couchdbUsername && couchdbPassword) { + const credentials = btoa(`${couchdbUsername}:${couchdbPassword}`); + headers['Authorization'] = `Basic ${credentials}`; + console.log(`Using authentication for user: ${couchdbUsername}`); + } + + const response = await fetch(couchdbUrl, { headers }); + + if (response.ok) { + const info = await response.json(); + console.log('✅ CouchDB connection successful:', info); + + // Test database access + const dbUrl = `${couchdbUrl}/commad-documents`; + const dbResponse = await fetch(dbUrl, { headers }); + + if (dbResponse.ok) { + const dbInfo = await dbResponse.json(); + console.log('✅ Database access successful:', dbInfo); + } else if (dbResponse.status === 404) { + console.log('â„šī¸ Database does not exist yet (will be created automatically)'); + } else if (dbResponse.status === 401) { + console.error('❌ 401 Unauthorized: Database access denied'); + console.log('💡 Try: commad.config.setAuth("username", "password")'); + } else { + console.log(`âš ī¸ Database response: ${dbResponse.status} ${dbResponse.statusText}`); + } + } else if (response.status === 401) { + console.error('❌ 401 Unauthorized: Authentication required'); + console.log('💡 Try: commad.config.setAuth("username", "password")'); + } else { + console.error(`❌ CouchDB connection failed: ${response.status} ${response.statusText}`); + } + } catch (error) { + console.error('❌ CouchDB connection error:', error.message); + + if (error.message.includes('Failed to fetch')) { + console.log('💡 Check that CouchDB is running and the URL is correct'); + console.log('💡 Default CouchDB URL: http://localhost:5984'); + } + } + } + }, + + // Sync management + sync: { + /** + * Show sync help + */ + help: () => { + console.log(` +🔄 Sync Management Commands +=========================== + +Status & Info: +â€ĸ commad.sync.status() - Get current sync status +â€ĸ commad.sync.info() - Show detailed sync information + +Manual Sync: +â€ĸ commad.sync.force() - Force a full sync +â€ĸ commad.sync.push() - Push local changes to remote +â€ĸ commad.sync.pull() - Pull changes from remote + +Connection Management: +â€ĸ commad.sync.start() - Start continuous sync +â€ĸ commad.sync.stop() - Stop sync +â€ĸ commad.sync.reconnect() - Reconnect to remote + +Conflict Resolution: +â€ĸ commad.sync.conflicts() - List documents with conflicts +â€ĸ commad.sync.resolve(id, winningRev, losingRevs) - Resolve conflict + +Examples: + commad.sync.status() + commad.sync.force() + commad.sync.conflicts() + `); + }, + + /** + * Get sync status + */ + status: () => { + const status = syncService.getStatus(); + console.log('📊 Sync Status:', status); + return status; + }, + + /** + * Show detailed sync information + */ + info: () => { + const status = syncService.getStatus(); + const config = configManager.getAll(); + + console.log(` +🔄 Sync Information +================== +Status: ${status.status} +Online: ${status.isOnline ? '✅' : '❌'} +Connected: ${status.isConnected ? '✅' : '❌'} +CouchDB URL: ${config.couchdbUrl} +Sync Enabled: ${config.syncEnabled ? '✅' : '❌'} +Last Sync: ${status.lastSyncTime || 'Never'} +Error: ${status.error || 'None'} +Sync Interval: ${config.syncInterval}ms + `); + + return status; + }, + + /** + * Force a full sync + */ + force: async () => { + console.log('🔄 Starting forced sync...'); + try { + const result = await DatabaseService.forceSync(); + console.log('✅ Sync completed successfully:', result); + return result; + } catch (error) { + console.error('❌ Sync failed:', error.message); + throw error; + } + }, + + /** + * Push local changes to remote + */ + push: async () => { + console.log('âŦ†ī¸ Pushing local changes...'); + try { + const result = await syncService.pushToRemote(); + console.log('✅ Push completed successfully:', result); + return result; + } catch (error) { + console.error('❌ Push failed:', error.message); + throw error; + } + }, + + /** + * Pull changes from remote + */ + pull: async () => { + console.log('âŦ‡ī¸ Pulling remote changes...'); + try { + const result = await syncService.pullFromRemote(); + console.log('✅ Pull completed successfully:', result); + return result; + } catch (error) { + console.error('❌ Pull failed:', error.message); + throw error; + } + }, + + /** + * Start continuous sync + */ + start: () => { + console.log('â–ļī¸ Starting continuous sync...'); + const config = configManager.getAll(); + if (!config.syncEnabled) { + configManager.set('syncEnabled', true); + console.log('✅ Sync enabled and started'); + } else { + console.log('â„šī¸ Sync is already enabled'); + } + }, + + /** + * Stop sync + */ + stop: () => { + console.log('âšī¸ Stopping sync...'); + configManager.set('syncEnabled', false); + console.log('✅ Sync stopped'); + }, + + /** + * Reconnect to remote + */ + reconnect: async () => { + console.log('🔄 Reconnecting to remote...'); + const config = configManager.getAll(); + + if (!config.couchdbUrl) { + console.error('❌ No CouchDB URL configured'); + return; + } + + // Toggle sync to force reconnection + configManager.set('syncEnabled', false); + setTimeout(() => { + configManager.set('syncEnabled', true); + console.log('✅ Reconnection initiated'); + }, 1000); + }, + + /** + * List documents with conflicts + */ + conflicts: async () => { + console.log('🔍 Checking for conflicts...'); + try { + const conflicts = await DatabaseService.getConflicts(); + + if (conflicts.length === 0) { + console.log('✅ No conflicts found'); + return []; + } + + console.log(`âš ī¸ Found ${conflicts.length} document(s) with conflicts:`); + conflicts.forEach(conflict => { + console.log(`- Document: ${conflict.id}`); + console.log(` Conflicts: ${conflict.conflicts.length} revision(s)`); + }); + + return conflicts; + } catch (error) { + console.error('❌ Error checking conflicts:', error.message); + throw error; + } + }, + + /** + * Resolve a document conflict + */ + resolve: async (docId, winningRev, losingRevs) => { + if (!docId || !winningRev || !losingRevs) { + console.log(` +Usage: commad.sync.resolve(docId, winningRev, losingRevs) + +Example: + commad.sync.resolve('doc1', '2-abc123', ['1-def456']) + `); + return; + } + + console.log(`🔧 Resolving conflict for document: ${docId}`); + try { + const result = await DatabaseService.resolveConflict(docId, winningRev, losingRevs); + + if (result) { + console.log('✅ Conflict resolved successfully'); + } else { + console.log('❌ Failed to resolve conflict'); + } + + return result; + } catch (error) { + console.error('❌ Error resolving conflict:', error.message); + throw error; + } + } + }, + + /** + * Show general help + */ + help: () => { + console.log(` +🚀 Commad Console Tools +======================= + +Available tool categories: +â€ĸ commad.config.* - Configuration management +â€ĸ commad.sync.* - Sync & CouchDB management +â€ĸ commad.utils.* - Utility functions +â€ĸ commad.help() - Show this help + +Quick start: +â€ĸ commad.config.help() - Configuration help +â€ĸ commad.sync.help() - Sync management help +â€ĸ commad.config.getAll() - View current config +â€ĸ commad.sync.status() - Check sync status +â€ĸ commad.utils.info() - Application info + +Type any command for detailed usage information. + `); + } +}; + +// Function to expose tools to global scope +export const exposeConsoleTools = () => { + // Only expose in development or if explicitly enabled + if (process.env.NODE_ENV === 'development' || configManager.get('enableConsoleTools')) { + window.commad = consoleTools; + console.log(` +🔧 Commad Console Tools Available! +Type 'commad.help()' to get started. + `); + } +}; + +export default consoleTools; diff --git a/vite.config.js b/vite.config.js index 09e615f..30a2fa1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,4 +8,22 @@ export default defineConfig({ optimizeDeps: { allowNodeBuiltins: ['pouchdb-browser', 'pouchdb-utils'] }, + server: { + proxy: { + // Proxy all requests to /db/* to CouchDB + '/db': { + target: process.env.COUCHDB_URL || 'http://localhost:5984', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/db/, ''), // /db/test -> /test + configure: (proxy, options) => { + proxy.on('error', (err, req, res) => { + console.log('Proxy error:', err); + }); + proxy.on('proxyReq', (proxyReq, req, res) => { + console.log('Proxying request:', req.method, req.url, '->', options.target + req.url.replace(/^\/db/, '')); + }); + } + } + } + } })