mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
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:
291
FLASK_BACKEND_SETUP.md
Normal file
291
FLASK_BACKEND_SETUP.md
Normal 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
|
||||
130
STORAGE.md
130
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 (
|
||||
<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
22
backend/.dockerignore
Normal 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
21
backend/Dockerfile
Normal 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
200
backend/README.md
Normal 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
184
backend/app.py
Normal 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
3
backend/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Flask==3.1.0
|
||||
Flask-CORS==5.0.0
|
||||
gunicorn==23.0.0
|
||||
@@ -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
48
package-lock.json
generated
@@ -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",
|
||||
|
||||
233
src/components/molecules/StorageSettings.tsx
Normal file
233
src/components/molecules/StorageSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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> = {}
|
||||
|
||||
Reference in New Issue
Block a user