From 519ad0016d2d4c5e903ab83a69cbe7ad94f09aea Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 17 Jan 2026 18:31:43 +0000 Subject: [PATCH] Generated by Spark: Ok I figured it out. Make a backend folder, put a flask backend in it with a Dockerfile. Main UI uses IndexedDB then with a UI setting, it can be moved to the flask backend. --- FLASK_BACKEND_SETUP.md | 291 +++++++++++++++++++ STORAGE.md | 130 ++++++--- backend/.dockerignore | 22 ++ backend/Dockerfile | 21 ++ backend/README.md | 200 +++++++++++++ backend/app.py | 184 ++++++++++++ backend/requirements.txt | 3 + docker-compose.yml | 28 +- package-lock.json | 48 ++- src/components/molecules/StorageSettings.tsx | 233 +++++++++++++++ src/components/molecules/index.ts | 1 + src/hooks/use-unified-storage.ts | 24 +- src/lib/unified-storage.ts | 128 +++++++- 13 files changed, 1227 insertions(+), 86 deletions(-) create mode 100644 FLASK_BACKEND_SETUP.md create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/app.py create mode 100644 backend/requirements.txt create mode 100644 src/components/molecules/StorageSettings.tsx diff --git a/FLASK_BACKEND_SETUP.md b/FLASK_BACKEND_SETUP.md new file mode 100644 index 0000000..6d6ed30 --- /dev/null +++ b/FLASK_BACKEND_SETUP.md @@ -0,0 +1,291 @@ +# Flask Backend Integration - Quick Start + +This guide explains how to use the Flask backend for persistent storage with CodeForge. + +## Overview + +CodeForge now supports multiple storage backends: +- **IndexedDB** (default) - Browser storage, works offline +- **Flask Backend** (optional) - Server storage, persistent across devices +- **SQLite** (optional) - Browser storage with SQL support +- **Spark KV** (fallback) - Cloud storage + +## Setup Flask Backend + +### Option 1: Docker (Recommended) + +1. **Start the backend with Docker Compose:** + ```bash + docker-compose up -d backend + ``` + +2. **Verify it's running:** + ```bash + curl http://localhost:5001/health + ``` + +3. **Configure in the UI:** + - Open CodeForge settings + - Find "Storage Backend" section + - Enter backend URL: `http://localhost:5001` + - Click "Use Flask" + +### Option 2: Run Locally + +1. **Install dependencies:** + ```bash + cd backend + pip install -r requirements.txt + ``` + +2. **Start the server:** + ```bash + python app.py + ``` + + Or with gunicorn: + ```bash + gunicorn --bind 0.0.0.0:5001 --workers 4 app:app + ``` + +3. **Configure in the UI** (same as Docker option) + +### Option 3: Docker Only Backend + +```bash +cd backend +docker build -t codeforge-backend . +docker run -d -p 5001:5001 -v codeforge-data:/data --name codeforge-backend codeforge-backend +``` + +## Using the Backend + +### In the UI + +1. **Open Settings** (or wherever StorageSettings component is added) +2. **Find "Storage Backend" section** +3. **Enter Flask URL:** `http://localhost:5001` (or your server URL) +4. **Click "Use Flask"** +5. All data will be migrated automatically + +### Programmatically + +```typescript +import { unifiedStorage } from '@/lib/unified-storage' + +// Switch to Flask backend +await unifiedStorage.switchToFlask('http://localhost:5001') + +// Check current backend +const backend = await unifiedStorage.getBackend() +console.log(backend) // 'flask' + +// Use storage as normal +await unifiedStorage.set('my-key', { foo: 'bar' }) +const value = await unifiedStorage.get('my-key') +``` + +## Configuration + +### Environment Variables + +Create a `.env` file in the backend directory: + +```env +PORT=5001 +DEBUG=false +DATABASE_PATH=/data/codeforge.db +``` + +### Custom Port + +```bash +# Docker +docker run -e PORT=8080 -p 8080:8080 ... + +# Python +PORT=8080 python app.py +``` + +### Data Persistence + +Data is stored in SQLite at `/data/codeforge.db`. Make sure to mount a volume: + +```bash +docker run -v $(pwd)/data:/data ... +``` + +## Production Deployment + +### Docker Compose (Full Stack) + +```bash +# Start both frontend and backend +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop all +docker-compose down +``` + +### Separate Deployment + +1. **Deploy backend:** + ```bash + docker-compose up -d backend + ``` + +2. **Deploy frontend with backend URL:** + ```bash + docker build -t codeforge-frontend . + docker run -d -p 80:80 \ + -e VITE_BACKEND_URL=https://api.yourdomain.com \ + codeforge-frontend + ``` + +3. **Configure CORS** if frontend and backend are on different domains + +## Switching Backends + +### From IndexedDB to Flask + +1. Click "Use Flask" in settings +2. Enter backend URL +3. All data migrates automatically + +### From Flask to IndexedDB + +1. Click "Use IndexedDB" in settings +2. All data downloads to browser +3. Can work offline + +### Export/Import + +Always available regardless of backend: + +```typescript +// Export backup +const data = await unifiedStorage.exportData() +const json = JSON.stringify(data, null, 2) +// Save to file + +// Import backup +await unifiedStorage.importData(parsedData) +``` + +## Troubleshooting + +### Backend not connecting + +1. **Check backend is running:** + ```bash + curl http://localhost:5001/health + # Should return: {"status":"ok","timestamp":"..."} + ``` + +2. **Check CORS:** Backend has CORS enabled by default + +3. **Check URL:** Make sure URL in settings matches backend + +4. **Check network:** Browser console will show connection errors + +### Data not persisting + +1. **Check volume mount:** + ```bash + docker inspect codeforge-backend | grep Mounts -A 10 + ``` + +2. **Check permissions:** + ```bash + ls -la ./data + ``` + +3. **Check database:** + ```bash + sqlite3 ./data/codeforge.db ".tables" + ``` + +### Port conflicts + +```bash +# Use different port +docker run -p 8080:5001 ... + +# Update URL in settings to match +http://localhost:8080 +``` + +## Security Considerations + +⚠️ **The default Flask backend has no authentication!** + +For production: +1. Add authentication (JWT, API keys, etc.) +2. Use HTTPS/TLS +3. Restrict CORS origins +4. Add rate limiting +5. Use environment-specific configs + +## API Endpoints + +The Flask backend exposes these endpoints: + +- `GET /health` - Health check +- `GET /api/storage/keys` - List all keys +- `GET /api/storage/` - Get value +- `PUT /api/storage/` - Set/update value +- `DELETE /api/storage/` - Delete value +- `POST /api/storage/clear` - Clear all data +- `GET /api/storage/export` - Export all data +- `POST /api/storage/import` - Import data +- `GET /api/storage/stats` - Get statistics + +See `backend/README.md` for detailed API documentation. + +## Benefits of Flask Backend + +✅ **Persistent across devices** - Access data from any device +✅ **Team collaboration** - Share data with team members +✅ **Backup/restore** - Centralized backup location +✅ **No size limits** - Limited only by server disk space +✅ **SQL queries** - Server-side SQLite for complex queries +✅ **Scalable** - Add more storage as needed + +## Comparison + +| Feature | IndexedDB | Flask Backend | SQLite | Spark KV | +|---------|-----------|---------------|--------|----------| +| Offline | ✅ Yes | ❌ No | ✅ Yes | ❌ No | +| Cross-device | ❌ No | ✅ Yes | ❌ No | ✅ Yes | +| Size limit | ~50MB+ | Unlimited | ~5MB | Unlimited | +| Speed | Fast | Moderate | Fast | Moderate | +| Setup | None | Docker/Server | npm install | Spark only | +| SQL queries | ❌ No | ✅ Yes | ✅ Yes | ❌ No | + +## Next Steps + +1. **Add to settings page:** + ```typescript + import { StorageSettings } from '@/components/molecules' + + function SettingsPage() { + return + } + ``` + +2. **Customize backend** - Modify `backend/app.py` as needed + +3. **Add authentication** - Secure your backend for production + +4. **Deploy to cloud** - Use AWS, Azure, DigitalOcean, etc. + +5. **Monitor usage** - Use `/api/storage/stats` endpoint + +## Support + +- Full documentation: `STORAGE.md` +- Backend docs: `backend/README.md` +- Issues: Open a GitHub issue diff --git a/STORAGE.md b/STORAGE.md index 735632d..1bd14e2 100644 --- a/STORAGE.md +++ b/STORAGE.md @@ -4,21 +4,22 @@ CodeForge now features a unified storage system that automatically selects the b ## Storage Backends -The system supports three storage backends in order of preference: +The system supports four storage backends in order of preference: -### 1. **SQLite (Preferred)** -- **Type**: On-disk database via WASM -- **Persistence**: Data stored in browser localStorage as serialized SQLite database +### 1. **Flask Backend (Optional)** +- **Type**: Remote HTTP API with SQLite database +- **Persistence**: Data stored on Flask server with SQLite - **Pros**: - - SQL query support - - Better performance for complex queries - - More robust data integrity - - Works offline + - Cross-device synchronization + - Centralized data management + - SQL query support on server + - Scalable storage capacity + - Works with Docker - **Cons**: - - Requires sql.js library (optional dependency) - - Slightly larger bundle size - - localStorage size limits (~5-10MB) -- **Installation**: `npm install sql.js` + - Requires running backend server + - Network latency + - Requires configuration +- **Setup**: See backend/README.md for installation ### 2. **IndexedDB (Default)** - **Type**: Browser-native key-value store @@ -34,7 +35,21 @@ The system supports three storage backends in order of preference: - More complex API - Asynchronous only -### 3. **Spark KV (Fallback)** +### 3. **SQLite (Optional)** +- **Type**: On-disk database via WASM +- **Persistence**: Data stored in browser localStorage as serialized SQLite database +- **Pros**: + - SQL query support + - Better performance for complex queries + - More robust data integrity + - Works offline +- **Cons**: + - Requires sql.js library (optional dependency) + - Slightly larger bundle size + - localStorage size limits (~5-10MB) +- **Installation**: `npm install sql.js` + +### 4. **Spark KV (Fallback)** - **Type**: Cloud key-value store - **Persistence**: Data stored in Spark runtime - **Pros**: @@ -110,8 +125,9 @@ function StorageManager() { const { backend, isLoading, - switchToSQLite, + switchToFlask, switchToIndexedDB, + switchToSQLite, exportData, importData, } = useStorageBackend() @@ -119,8 +135,11 @@ function StorageManager() { return (

Current backend: {backend}

- + + +
+

+ Store data on a Flask server (persistent across devices) +

+ + +
+ + +
+ +
+

IndexedDB (Default): Browser storage, large capacity, works offline

+

SQLite: Browser storage with SQL queries, requires sql.js package

+

Flask: Server storage, persistent across devices, requires backend

+
+ + + + + + + Data Management + + Export or import your data + + + +
+ + +
+

+ Backup your data to a JSON file or restore from a previous backup +

+
+
+ + ) +} diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index 0c6bb2f..f04b720 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -12,6 +12,7 @@ export { LazyMonacoEditor, preloadMonacoEditor } from './LazyMonacoEditor' export { LazyLineChart } from './LazyLineChart' export { LazyBarChart } from './LazyBarChart' export { LazyD3BarChart } from './LazyD3BarChart' +export { StorageSettings } from './StorageSettings' export { LoadingFallback } from './LoadingFallback' export { MonacoEditorPanel } from './MonacoEditorPanel' export { NavigationGroupHeader } from './NavigationGroupHeader' diff --git a/src/hooks/use-unified-storage.ts b/src/hooks/use-unified-storage.ts index cbce30e..80e6f20 100644 --- a/src/hooks/use-unified-storage.ts +++ b/src/hooks/use-unified-storage.ts @@ -95,13 +95,13 @@ export function useStorageBackend() { } }, []) - const switchToSQLite = useCallback(async () => { + const switchToFlask = useCallback(async (backendUrl?: string) => { setIsLoading(true) try { - await unifiedStorage.switchToSQLite() - setBackend('sqlite') + await unifiedStorage.switchToFlask(backendUrl) + setBackend('flask') } catch (error) { - console.error('Failed to switch to SQLite:', error) + console.error('Failed to switch to Flask:', error) throw error } finally { setIsLoading(false) @@ -121,6 +121,19 @@ export function useStorageBackend() { } }, []) + const switchToSQLite = useCallback(async () => { + setIsLoading(true) + try { + await unifiedStorage.switchToSQLite() + setBackend('sqlite') + } catch (error) { + console.error('Failed to switch to SQLite:', error) + throw error + } finally { + setIsLoading(false) + } + }, []) + const exportData = useCallback(async () => { try { return await unifiedStorage.exportData() @@ -145,8 +158,9 @@ export function useStorageBackend() { return { backend, isLoading, - switchToSQLite, + switchToFlask, switchToIndexedDB, + switchToSQLite, exportData, importData, } diff --git a/src/lib/unified-storage.ts b/src/lib/unified-storage.ts index 8c73788..3a27524 100644 --- a/src/lib/unified-storage.ts +++ b/src/lib/unified-storage.ts @@ -1,4 +1,4 @@ -export type StorageBackend = 'sqlite' | 'indexeddb' | 'sparkkv' +export type StorageBackend = 'flask' | 'indexeddb' | 'sqlite' | 'sparkkv' export interface StorageAdapter { get(key: string): Promise @@ -9,6 +9,67 @@ export interface StorageAdapter { close?(): Promise } +class FlaskBackendAdapter implements StorageAdapter { + private baseUrl: string + + constructor(baseUrl?: string) { + this.baseUrl = baseUrl || localStorage.getItem('codeforge-flask-url') || 'http://localhost:5001' + } + + private async request(endpoint: string, options?: RequestInit): Promise { + const response = await fetch(`${this.baseUrl}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: response.statusText })) + throw new Error(error.error || `HTTP ${response.status}`) + } + + return response.json() + } + + async get(key: string): Promise { + try { + const result = await this.request<{ value: T }>(`/api/storage/${encodeURIComponent(key)}`) + return result.value + } catch (error: any) { + if (error.message?.includes('404') || error.message?.includes('not found')) { + return undefined + } + throw error + } + } + + async set(key: string, value: T): Promise { + await this.request(`/api/storage/${encodeURIComponent(key)}`, { + method: 'PUT', + body: JSON.stringify({ value }), + }) + } + + async delete(key: string): Promise { + await this.request(`/api/storage/${encodeURIComponent(key)}`, { + method: 'DELETE', + }) + } + + async keys(): Promise { + const result = await this.request<{ keys: string[] }>('/api/storage/keys') + return result.keys + } + + async clear(): Promise { + await this.request('/api/storage/clear', { + method: 'POST', + }) + } +} + class IndexedDBAdapter implements StorageAdapter { private db: IDBDatabase | null = null private readonly dbName = 'CodeForgeDB' @@ -278,19 +339,20 @@ class UnifiedStorage { if (this.initPromise) return this.initPromise this.initPromise = (async () => { + const preferFlask = localStorage.getItem('codeforge-prefer-flask') === 'true' const preferSQLite = localStorage.getItem('codeforge-prefer-sqlite') === 'true' - if (preferSQLite) { + if (preferFlask) { try { - console.log('[Storage] Attempting to initialize SQLite...') - const sqliteAdapter = new SQLiteAdapter() - await sqliteAdapter.get('_health_check') - this.adapter = sqliteAdapter - this.backend = 'sqlite' - console.log('[Storage] ✓ Using SQLite') + console.log('[Storage] Attempting to initialize Flask backend...') + const flaskAdapter = new FlaskBackendAdapter() + await flaskAdapter.get('_health_check') + this.adapter = flaskAdapter + this.backend = 'flask' + console.log('[Storage] ✓ Using Flask backend') return } catch (error) { - console.warn('[Storage] SQLite not available:', error) + console.warn('[Storage] Flask backend not available:', error) } } @@ -308,6 +370,20 @@ class UnifiedStorage { } } + if (preferSQLite) { + try { + console.log('[Storage] Attempting to initialize SQLite...') + const sqliteAdapter = new SQLiteAdapter() + await sqliteAdapter.get('_health_check') + this.adapter = sqliteAdapter + this.backend = 'sqlite' + console.log('[Storage] ✓ Using SQLite') + return + } catch (error) { + console.warn('[Storage] SQLite not available:', error) + } + } + if (window.spark?.kv) { try { console.log('[Storage] Attempting to initialize Spark KV...') @@ -408,6 +484,7 @@ class UnifiedStorage { this.initPromise = null localStorage.removeItem('codeforge-prefer-sqlite') + localStorage.removeItem('codeforge-prefer-flask') await this.detectAndInitialize() @@ -418,6 +495,39 @@ class UnifiedStorage { console.log('[Storage] ✓ Migrated to IndexedDB') } + async switchToFlask(backendUrl?: string): Promise { + if (this.backend === 'flask') return + + console.log('[Storage] Switching to Flask backend...') + const oldKeys = await this.keys() + const data: Record = {} + + for (const key of oldKeys) { + data[key] = await this.get(key) + } + + if (this.adapter?.close) { + await this.adapter.close() + } + + this.adapter = null + this.backend = null + this.initPromise = null + + localStorage.setItem('codeforge-prefer-flask', 'true') + if (backendUrl) { + localStorage.setItem('codeforge-flask-url', backendUrl) + } + + await this.detectAndInitialize() + + for (const [key, value] of Object.entries(data)) { + await this.set(key, value) + } + + console.log('[Storage] ✓ Migrated to Flask backend') + } + async exportData(): Promise> { const allKeys = await this.keys() const data: Record = {}