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.

This commit is contained in:
2026-01-17 18:31:43 +00:00
committed by GitHub
parent 02eb47e83f
commit 519ad0016d
13 changed files with 1227 additions and 86 deletions

291
FLASK_BACKEND_SETUP.md Normal file
View File

@@ -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/<key>` - Get value
- `PUT /api/storage/<key>` - Set/update value
- `DELETE /api/storage/<key>` - 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 <StorageSettings />
}
```
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

View File

@@ -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 (
<div>
<p>Current backend: {backend}</p>
<button onClick={switchToSQLite}>Switch to SQLite</button>
<button onClick={() => switchToFlask('http://localhost:5001')}>
Switch to Flask
</button>
<button onClick={switchToIndexedDB}>Switch to IndexedDB</button>
<button onClick={switchToSQLite}>Switch to SQLite</button>
<button onClick={async () => {
const data = await exportData()
console.log('Exported:', data)
@@ -137,11 +156,14 @@ function StorageManager() {
The system supports seamless migration between storage backends:
```typescript
// Migrate from IndexedDB to SQLite (preserves all data)
await unifiedStorage.switchToSQLite()
// Migrate to Flask backend
await unifiedStorage.switchToFlask('http://localhost:5001')
// Migrate from SQLite to IndexedDB (preserves all data)
// Migrate to IndexedDB (preserves all data)
await unifiedStorage.switchToIndexedDB()
// Migrate to SQLite (preserves all data)
await unifiedStorage.switchToSQLite()
```
When switching backends:
@@ -176,31 +198,38 @@ await unifiedStorage.importData(imported)
The system automatically detects and selects the best available backend on initialization:
1. **SQLite** is attempted first if `localStorage.getItem('codeforge-prefer-sqlite') === 'true'`
1. **Flask** is attempted first if `localStorage.getItem('codeforge-prefer-flask') === 'true'`
2. **IndexedDB** is attempted next if available in the browser
3. **Spark KV** is used as a last resort fallback
3. **SQLite** is attempted if `localStorage.getItem('codeforge-prefer-sqlite') === 'true'`
4. **Spark KV** is used as a last resort fallback
You can check which backend is in use:
```typescript
const backend = await unifiedStorage.getBackend()
// Returns: 'sqlite' | 'indexeddb' | 'sparkkv' | null
// Returns: 'flask' | 'indexeddb' | 'sqlite' | 'sparkkv' | null
```
## Performance Considerations
### SQLite
- Best for: Complex queries, relational data, large datasets
- Read: Fast (in-memory queries)
- Write: Moderate (requires serialization to localStorage)
- Capacity: Limited by localStorage (~5-10MB)
### Flask Backend
- Best for: Cross-device sync, centralized data, team collaboration
- Read: Moderate (network latency)
- Write: Moderate (network latency)
- Capacity: Large (server disk space)
### IndexedDB
- Best for: Simple key-value storage, large data volumes
- Best for: Simple key-value storage, large data volumes, offline-first
- Read: Very fast (optimized for key lookups)
- Write: Very fast (optimized browser API)
- Capacity: Large (typically 50MB+, can scale to GBs)
### SQLite
- Best for: Complex queries, relational data, SQL support
- Read: Fast (in-memory queries)
- Write: Moderate (requires serialization to localStorage)
- Capacity: Limited by localStorage (~5-10MB)
### Spark KV
- Best for: Cross-device sync, cloud persistence
- Read: Moderate (network latency)
@@ -209,6 +238,15 @@ const backend = await unifiedStorage.getBackend()
## Troubleshooting
### Flask Backend Not Available
If Flask backend fails to connect:
1. Check backend is running: `curl http://localhost:5001/health`
2. Verify CORS is enabled on backend
3. Check the Flask URL is correct in settings
4. System will automatically fallback to IndexedDB
5. See backend/README.md for backend setup
### SQLite Not Available
If SQLite fails to initialize:
@@ -221,7 +259,7 @@ If SQLite fails to initialize:
If IndexedDB storage is full:
1. Clear old data: `await unifiedStorage.clear()`
2. Export important data first
3. Consider switching to Spark KV for unlimited storage
3. Consider switching to Flask backend for unlimited storage
### Data Not Persisting
@@ -258,28 +296,30 @@ If IndexedDB storage is full:
```
4. **Use Appropriate Backend**: Choose based on your needs:
- Team collaboration, cross-device → Flask backend
- Local-only, small data → IndexedDB
- Local-only, needs SQL → SQLite (install sql.js)
- Cloud sync needed → Spark KV
## UI Component
The app includes a `StorageSettingsPanel` component that provides a user-friendly interface for:
The app includes a `StorageSettings` component that provides a user-friendly interface for:
- Viewing current storage backend
- Switching between backends
- Switching between backends (Flask, IndexedDB, SQLite)
- Configuring Flask backend URL
- Exporting/importing data
- Viewing storage statistics
Add it to your settings page:
```typescript
import { StorageSettingsPanel } from '@/components/StorageSettingsPanel'
import { StorageSettings } from '@/components/molecules'
function SettingsPage() {
return (
<div>
<h1>Settings</h1>
<StorageSettingsPanel />
<StorageSettings />
</div>
)
}
@@ -295,18 +335,19 @@ function SettingsPage() {
├─ Automatic Backend Detection
┌───────┴───────┬─────────────┬────────┐
┌───────┴───────┬─────────────┬────────┬────────
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────────┐ ┌─────────────┐ ┌─────────┐ ┌────┐ ┌────┐
│ Flask │ │ IndexedDB │ │ SQLite │ │ KV │ │ ? │
│ (optional)│ │ (default) │ │(optional│ │ │ │Next│
└────────────┘ └─────────────┘ └─────────┘ └────┘ └────┘
│ │ │ │
▼ ▼ ▼
┌─────────────┐ ┌────────────┐ ┌─────────┐ ┌────┐
SQLite │ │ IndexedDB │ │Spark KV │ │ ?
│ (optional) │ │ (default) │ │(fallback│ │Next│
└─────────────┘ └────────────┘ └─────────┘ └────┘
│ │ │
└───────┬───────┴─────────────┘
Browser Storage
└─────┬───────┴────────┘
│ │
HTTP Server Browser Storage
(SQLite)
```
## Future Enhancements
@@ -318,3 +359,6 @@ function SettingsPage() {
- [ ] Encrypted storage option
- [ ] Storage analytics and usage metrics
- [ ] Automatic data migration on version changes
- [ ] Flask backend authentication/authorization
- [ ] Multi-user support with Flask backend
- [ ] Real-time sync with WebSockets

22
backend/.dockerignore Normal file
View File

@@ -0,0 +1,22 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
*.db
*.sqlite
*.sqlite3
.env
.venv
venv/
ENV/
.git
.gitignore
README.md
*.md
.DS_Store

21
backend/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN mkdir -p /data
ENV PORT=5001
ENV DEBUG=false
ENV DATABASE_PATH=/data/codeforge.db
EXPOSE 5001
VOLUME ["/data"]
CMD ["gunicorn", "--bind", "0.0.0.0:5001", "--workers", "4", "--timeout", "120", "app:app"]

200
backend/README.md Normal file
View File

@@ -0,0 +1,200 @@
# CodeForge Flask Backend
A Flask-based storage backend for CodeForge that provides persistent storage using SQLite.
## Features
- RESTful API for key-value storage
- SQLite database for data persistence
- CORS enabled for frontend communication
- Data import/export functionality
- Health check endpoint
- Storage statistics
## API Endpoints
### Health Check
```
GET /health
```
### Storage Operations
#### Get all keys
```
GET /api/storage/keys
Response: { "keys": ["key1", "key2", ...] }
```
#### Get value by key
```
GET /api/storage/<key>
Response: { "value": {...} }
```
#### Set/Update value
```
PUT /api/storage/<key>
POST /api/storage/<key>
Body: { "value": {...} }
Response: { "success": true }
```
#### Delete value
```
DELETE /api/storage/<key>
Response: { "success": true }
```
#### Clear all data
```
POST /api/storage/clear
Response: { "success": true }
```
#### Export all data
```
GET /api/storage/export
Response: { "key1": value1, "key2": value2, ... }
```
#### Import data
```
POST /api/storage/import
Body: { "key1": value1, "key2": value2, ... }
Response: { "success": true, "imported": count }
```
#### Get storage statistics
```
GET /api/storage/stats
Response: {
"total_keys": 42,
"total_size_bytes": 123456,
"database_path": "/data/codeforge.db"
}
```
## Environment Variables
- `PORT`: Server port (default: 5001)
- `DEBUG`: Enable debug mode (default: false)
- `DATABASE_PATH`: SQLite database file path (default: /data/codeforge.db)
## Running with Docker
### Build the image
```bash
docker build -t codeforge-backend ./backend
```
### Run the container
```bash
docker run -d \
-p 5001:5001 \
-v codeforge-data:/data \
--name codeforge-backend \
codeforge-backend
```
### With custom settings
```bash
docker run -d \
-p 8080:8080 \
-e PORT=8080 \
-e DEBUG=true \
-v $(pwd)/data:/data \
--name codeforge-backend \
codeforge-backend
```
## Running without Docker
### Install dependencies
```bash
cd backend
pip install -r requirements.txt
```
### Development mode
```bash
python app.py
```
### Production mode with gunicorn
```bash
gunicorn --bind 0.0.0.0:5001 --workers 4 app:app
```
## Docker Compose
Add to your `docker-compose.yml`:
```yaml
version: '3.8'
services:
backend:
build: ./backend
ports:
- "5001:5001"
volumes:
- codeforge-data:/data
environment:
- PORT=5001
- DEBUG=false
- DATABASE_PATH=/data/codeforge.db
restart: unless-stopped
volumes:
codeforge-data:
```
Run with:
```bash
docker-compose up -d
```
## Data Persistence
The SQLite database is stored in `/data/codeforge.db` inside the container. Mount a volume to persist data:
```bash
# Named volume (recommended)
-v codeforge-data:/data
# Bind mount
-v $(pwd)/data:/data
```
## Security Considerations
- This backend is designed for local/internal use
- No authentication is implemented by default
- CORS is enabled for all origins
- For production use, consider adding:
- Authentication/authorization
- Rate limiting
- HTTPS/TLS
- Restricted CORS origins
- Input validation/sanitization
## Troubleshooting
### Port already in use
Change the port mapping:
```bash
docker run -p 8080:5001 ...
```
### Permission denied on /data
Ensure the volume has proper permissions:
```bash
docker run --user $(id -u):$(id -g) ...
```
### Cannot connect from frontend
Check:
1. Backend is running: `curl http://localhost:5001/health`
2. CORS is enabled (it should be by default)
3. Frontend BACKEND_URL environment variable is set correctly

184
backend/app.py Normal file
View File

@@ -0,0 +1,184 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import sqlite3
import json
import os
from datetime import datetime
from contextlib import contextmanager
app = Flask(__name__)
CORS(app)
DATABASE_PATH = os.environ.get('DATABASE_PATH', '/data/codeforge.db')
os.makedirs(os.path.dirname(DATABASE_PATH), exist_ok=True)
@contextmanager
def get_db():
conn = sqlite3.connect(DATABASE_PATH)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
def init_db():
with get_db() as conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS storage (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_updated_at ON storage(updated_at)
''')
conn.commit()
init_db()
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'ok', 'timestamp': datetime.utcnow().isoformat()})
@app.route('/api/storage/keys', methods=['GET'])
def get_keys():
try:
with get_db() as conn:
cursor = conn.execute('SELECT key FROM storage ORDER BY key')
keys = [row['key'] for row in cursor.fetchall()]
return jsonify({'keys': keys})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/storage/<key>', methods=['GET'])
def get_value(key):
try:
with get_db() as conn:
cursor = conn.execute('SELECT value FROM storage WHERE key = ?', (key,))
row = cursor.fetchone()
if row is None:
return jsonify({'error': 'Key not found'}), 404
return jsonify({'value': json.loads(row['value'])})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/storage/<key>', methods=['PUT', 'POST'])
def set_value(key):
try:
data = request.get_json()
if 'value' not in data:
return jsonify({'error': 'Missing value field'}), 400
value_json = json.dumps(data['value'])
now = datetime.utcnow().isoformat()
with get_db() as conn:
cursor = conn.execute('SELECT key FROM storage WHERE key = ?', (key,))
exists = cursor.fetchone() is not None
if exists:
conn.execute(
'UPDATE storage SET value = ?, updated_at = ? WHERE key = ?',
(value_json, now, key)
)
else:
conn.execute(
'INSERT INTO storage (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)',
(key, value_json, now, now)
)
conn.commit()
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/storage/<key>', methods=['DELETE'])
def delete_value(key):
try:
with get_db() as conn:
cursor = conn.execute('DELETE FROM storage WHERE key = ?', (key,))
conn.commit()
if cursor.rowcount == 0:
return jsonify({'error': 'Key not found'}), 404
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/storage/clear', methods=['POST'])
def clear_all():
try:
with get_db() as conn:
conn.execute('DELETE FROM storage')
conn.commit()
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/storage/export', methods=['GET'])
def export_data():
try:
with get_db() as conn:
cursor = conn.execute('SELECT key, value FROM storage')
data = {row['key']: json.loads(row['value']) for row in cursor.fetchall()}
return jsonify(data)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/storage/import', methods=['POST'])
def import_data():
try:
data = request.get_json()
if not isinstance(data, dict):
return jsonify({'error': 'Data must be an object'}), 400
now = datetime.utcnow().isoformat()
with get_db() as conn:
for key, value in data.items():
value_json = json.dumps(value)
cursor = conn.execute('SELECT key FROM storage WHERE key = ?', (key,))
exists = cursor.fetchone() is not None
if exists:
conn.execute(
'UPDATE storage SET value = ?, updated_at = ? WHERE key = ?',
(value_json, now, key)
)
else:
conn.execute(
'INSERT INTO storage (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)',
(key, value_json, now, now)
)
conn.commit()
return jsonify({'success': True, 'imported': len(data)})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/storage/stats', methods=['GET'])
def get_stats():
try:
with get_db() as conn:
cursor = conn.execute('SELECT COUNT(*) as count FROM storage')
count = cursor.fetchone()['count']
cursor = conn.execute('SELECT SUM(LENGTH(value)) as total_size FROM storage')
total_size = cursor.fetchone()['total_size'] or 0
return jsonify({
'total_keys': count,
'total_size_bytes': total_size,
'database_path': DATABASE_PATH
})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5001))
app.run(host='0.0.0.0', port=port, debug=os.environ.get('DEBUG', 'false').lower() == 'true')

3
backend/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
Flask==3.1.0
Flask-CORS==5.0.0
gunicorn==23.0.0

View File

@@ -1,7 +1,7 @@
version: '3.8'
services:
app:
frontend:
build:
context: .
dockerfile: Dockerfile
@@ -9,6 +9,9 @@ services:
- '3000:80'
environment:
- NODE_ENV=production
- VITE_BACKEND_URL=http://backend:5001
depends_on:
- backend
restart: unless-stopped
healthcheck:
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost/health']
@@ -21,6 +24,29 @@ services:
- 'traefik.http.routers.codeforge.rule=Host(`codeforge.example.com`)'
- 'traefik.http.services.codeforge.loadbalancer.server.port=80'
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "5001:5001"
volumes:
- codeforge-data:/data
environment:
- PORT=5001
- DEBUG=false
- DATABASE_PATH=/data/codeforge.db
restart: unless-stopped
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:5001/health']
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
volumes:
codeforge-data:
networks:
default:
name: codeforge-network

48
package-lock.json generated
View File

@@ -135,6 +135,7 @@
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
@@ -150,6 +151,7 @@
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -249,7 +251,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -273,7 +274,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1168,7 +1168,6 @@
"node_modules/@octokit/core": {
"version": "6.1.6",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^5.0.0",
"@octokit/graphql": "^8.2.2",
@@ -4556,7 +4555,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/aws-lambda": {
"version": "8.10.159",
@@ -4863,7 +4863,6 @@
"version": "19.2.7",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4872,7 +4871,6 @@
"version": "19.2.3",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -4908,7 +4906,8 @@
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.0",
@@ -4950,7 +4949,6 @@
"version": "8.48.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.0",
"@typescript-eslint/types": "8.48.0",
@@ -5383,7 +5381,6 @@
"version": "8.15.0",
"devOptional": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5430,6 +5427,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -6114,7 +6112,6 @@
"node_modules/d3-selection": {
"version": "3.0.0",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -6342,7 +6339,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -6355,6 +6353,7 @@
"node_modules/dompurify": {
"version": "3.2.7",
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -6381,8 +6380,7 @@
},
"node_modules/embla-carousel": {
"version": "8.6.0",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -6548,7 +6546,6 @@
"version": "9.39.2",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7698,7 +7695,6 @@
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.1.0",
"data-urls": "^5.0.0",
@@ -8089,6 +8085,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -8226,6 +8223,7 @@
"node_modules/monaco-editor": {
"version": "0.55.1",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
@@ -8234,6 +8232,7 @@
"node_modules/monaco-editor/node_modules/marked": {
"version": "14.0.0",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -8620,6 +8619,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -8635,6 +8635,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -8647,7 +8648,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
@@ -8760,7 +8762,6 @@
"node_modules/react": {
"version": "19.2.0",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8795,7 +8796,6 @@
"node_modules/react-dom": {
"version": "19.2.0",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -8816,7 +8816,6 @@
"node_modules/react-hook-form": {
"version": "7.67.0",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9114,7 +9113,6 @@
"node_modules/rollup": {
"version": "4.53.3",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -9269,7 +9267,6 @@
"node_modules/sass": {
"version": "1.97.2",
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -9680,8 +9677,7 @@
},
"node_modules/tailwindcss": {
"version": "4.1.17",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -9870,8 +9866,7 @@
},
"node_modules/tslib": {
"version": "2.8.1",
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tw-animate-css": {
"version": "1.4.0",
@@ -9929,7 +9924,6 @@
"version": "5.7.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10117,7 +10111,6 @@
"node_modules/vite": {
"version": "7.3.1",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -10706,7 +10699,6 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",

View File

@@ -0,0 +1,233 @@
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { useStorageBackend } from '@/hooks/use-unified-storage'
import { toast } from 'sonner'
import { Database, HardDrive, Cloud, Cpu, Download, Upload } from '@phosphor-icons/react'
export function StorageSettings() {
const {
backend,
isLoading,
switchToFlask,
switchToIndexedDB,
switchToSQLite,
exportData,
importData,
} = useStorageBackend()
const [flaskUrl, setFlaskUrl] = useState(
localStorage.getItem('codeforge-flask-url') || 'http://localhost:5001'
)
const [isSwitching, setIsSwitching] = useState(false)
const handleSwitchToFlask = async () => {
setIsSwitching(true)
try {
await switchToFlask(flaskUrl)
toast.success('Switched to Flask backend')
} catch (error) {
toast.error(`Failed to switch to Flask: ${error}`)
} finally {
setIsSwitching(false)
}
}
const handleSwitchToIndexedDB = async () => {
setIsSwitching(true)
try {
await switchToIndexedDB()
toast.success('Switched to IndexedDB')
} catch (error) {
toast.error(`Failed to switch to IndexedDB: ${error}`)
} finally {
setIsSwitching(false)
}
}
const handleSwitchToSQLite = async () => {
setIsSwitching(true)
try {
await switchToSQLite()
toast.success('Switched to SQLite')
} catch (error) {
toast.error(`Failed to switch to SQLite: ${error}`)
} finally {
setIsSwitching(false)
}
}
const handleExport = async () => {
try {
const data = await exportData()
const json = JSON.stringify(data, null, 2)
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `codeforge-backup-${Date.now()}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success('Data exported successfully')
} catch (error) {
toast.error(`Failed to export data: ${error}`)
}
}
const handleImport = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
try {
const text = await file.text()
const data = JSON.parse(text)
await importData(data)
toast.success('Data imported successfully')
} catch (error) {
toast.error(`Failed to import data: ${error}`)
}
}
input.click()
}
const getBackendIcon = (backendType: string | null) => {
switch (backendType) {
case 'flask':
return <Cpu className="w-5 h-5" />
case 'indexeddb':
return <HardDrive className="w-5 h-5" />
case 'sqlite':
return <Database className="w-5 h-5" />
case 'sparkkv':
return <Cloud className="w-5 h-5" />
default:
return <Database className="w-5 h-5" />
}
}
const getBackendLabel = (backendType: string | null) => {
switch (backendType) {
case 'flask':
return 'Flask Backend'
case 'indexeddb':
return 'IndexedDB'
case 'sqlite':
return 'SQLite'
case 'sparkkv':
return 'Spark KV'
default:
return 'Unknown'
}
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{getBackendIcon(backend)}
Storage Backend
</CardTitle>
<CardDescription>
Choose where your data is stored
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Current backend:</span>
<Badge variant="secondary" className="flex items-center gap-1">
{getBackendIcon(backend)}
{getBackendLabel(backend)}
</Badge>
</div>
<div className="grid gap-4">
<div className="space-y-2">
<Label htmlFor="flask-url">Flask Backend URL</Label>
<div className="flex gap-2">
<Input
id="flask-url"
value={flaskUrl}
onChange={(e) => setFlaskUrl(e.target.value)}
placeholder="http://localhost:5001"
disabled={isSwitching || isLoading}
/>
<Button
onClick={handleSwitchToFlask}
disabled={isSwitching || isLoading || backend === 'flask'}
variant={backend === 'flask' ? 'secondary' : 'default'}
>
<Cpu className="w-4 h-4 mr-2" />
{backend === 'flask' ? 'Active' : 'Use Flask'}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Store data on a Flask server (persistent across devices)
</p>
</div>
<div className="flex gap-2">
<Button
onClick={handleSwitchToIndexedDB}
disabled={isSwitching || isLoading || backend === 'indexeddb'}
variant={backend === 'indexeddb' ? 'secondary' : 'outline'}
className="flex-1"
>
<HardDrive className="w-4 h-4 mr-2" />
{backend === 'indexeddb' ? 'Active' : 'Use IndexedDB'}
</Button>
<Button
onClick={handleSwitchToSQLite}
disabled={isSwitching || isLoading || backend === 'sqlite'}
variant={backend === 'sqlite' ? 'secondary' : 'outline'}
className="flex-1"
>
<Database className="w-4 h-4 mr-2" />
{backend === 'sqlite' ? 'Active' : 'Use SQLite'}
</Button>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p><strong>IndexedDB (Default):</strong> Browser storage, large capacity, works offline</p>
<p><strong>SQLite:</strong> Browser storage with SQL queries, requires sql.js package</p>
<p><strong>Flask:</strong> Server storage, persistent across devices, requires backend</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Data Management</CardTitle>
<CardDescription>
Export or import your data
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Button onClick={handleExport} variant="outline" className="flex-1">
<Download className="w-4 h-4 mr-2" />
Export Data
</Button>
<Button onClick={handleImport} variant="outline" className="flex-1">
<Upload className="w-4 h-4 mr-2" />
Import Data
</Button>
</div>
<p className="text-xs text-muted-foreground">
Backup your data to a JSON file or restore from a previous backup
</p>
</CardContent>
</Card>
</div>
)
}

View File

@@ -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'

View File

@@ -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,
}

View File

@@ -1,4 +1,4 @@
export type StorageBackend = 'sqlite' | 'indexeddb' | 'sparkkv'
export type StorageBackend = 'flask' | 'indexeddb' | 'sqlite' | 'sparkkv'
export interface StorageAdapter {
get<T>(key: string): Promise<T | undefined>
@@ -9,6 +9,67 @@ export interface StorageAdapter {
close?(): Promise<void>
}
class FlaskBackendAdapter implements StorageAdapter {
private baseUrl: string
constructor(baseUrl?: string) {
this.baseUrl = baseUrl || localStorage.getItem('codeforge-flask-url') || 'http://localhost:5001'
}
private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
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<T>(key: string): Promise<T | undefined> {
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<T>(key: string, value: T): Promise<void> {
await this.request(`/api/storage/${encodeURIComponent(key)}`, {
method: 'PUT',
body: JSON.stringify({ value }),
})
}
async delete(key: string): Promise<void> {
await this.request(`/api/storage/${encodeURIComponent(key)}`, {
method: 'DELETE',
})
}
async keys(): Promise<string[]> {
const result = await this.request<{ keys: string[] }>('/api/storage/keys')
return result.keys
}
async clear(): Promise<void> {
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<void> {
if (this.backend === 'flask') return
console.log('[Storage] Switching to Flask backend...')
const oldKeys = await this.keys()
const data: Record<string, any> = {}
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<Record<string, any>> {
const allKeys = await this.keys()
const data: Record<string, any> = {}