mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 07:57:19 -05:00
Integrate BlackRoad OS front-end with FastAPI backend
This commit transforms the BlackRoad OS from a static mockup into a fully functional web-based operating system with real backend integration. ## Major Changes ### Backend (New Features) 1. **Device Management System** (IoT/Raspberry Pi) - New models: Device, DeviceMetric, DeviceLog - Router: /api/devices with full CRUD operations - Device heartbeat system for status monitoring - Metrics tracking (CPU, RAM, temperature) 2. **Mining Stats & Control** (RoadCoin Miner) - Router: /api/miner with status, stats, control endpoints - Simulated mining with hashrate, shares, temperature - Start/stop mining controls - Lifetime statistics and recent blocks listing 3. **Static File Serving** - Backend now serves front-end from /backend/static/ - index.html served at root URL - API routes under /api/* namespace 4. **Updated User Model** - Added devices relationship ### Frontend (New Features) 1. **API Client Module** (api-client.js) - Centralized API communication layer - Automatic base URL detection (dev vs prod) - JWT token management with auto-refresh - Error handling and 401 redirects 2. **Authentication System** (auth.js) - Login/Register modal UI - Session persistence via localStorage - Auto-logout on token expiration - Keyboard shortcuts (Enter to submit) 3. **Application Modules** (apps.js) - Dynamic data loading for all desktop windows - Auto-refresh for real-time data (miner, blockchain) - Event-driven architecture - Lazy loading (data fetched only when window opens) 4. **Enhanced UI** - Added 380+ lines of CSS for new components - Auth modal styling - Miner dashboard layout - Blockchain explorer tables - Wallet balance display - Device management cards 5. **Live Window Integration** - RoadCoin Miner: Real mining stats, start/stop controls - RoadChain Explorer: Live blockchain data, mine block button - Wallet: Real-time balance, transaction history - Raspberry Pi: Device status dashboard - RoadMail: Live inbox from API - Social Feed: Real posts from database - BlackStream: Video grid from API - AI Assistant: Conversation UI ### Configuration - Updated .env.example with: - ROADCHAIN_RPC_URL, ROADCOIN_POOL_URL - MQTT broker settings for device management - Production CORS origins (www.blackroad.systems) - PORT configuration for Railway deployment ### Documentation - Added INTEGRATION_GUIDE.md (400+ lines) - Complete architecture overview - API endpoint documentation - Environment configuration guide - Development workflow - Troubleshooting section ## Technical Details - All windows now connect to real backend APIs - Authentication required before OS access - User-specific data isolation - Proper error handling and loading states - Retro Windows 95 aesthetic preserved ## What's Working ✅ Full authentication flow (login/register) ✅ Mining stats and control ✅ Blockchain explorer with live data ✅ Wallet with real balance ✅ Device management dashboard ✅ Email inbox integration ✅ Social feed integration ✅ Video platform integration ✅ Static file serving ✅ CORS configuration ## Future Enhancements - Real XMRig integration - WebSocket for real-time updates - MQTT broker for device heartbeats - OpenAI/Anthropic API integration - File uploads to S3 - Email sending via SMTP ## Files Added - backend/app/models/device.py - backend/app/routers/devices.py - backend/app/routers/miner.py - backend/static/index.html - backend/static/js/api-client.js - backend/static/js/auth.js - backend/static/js/apps.js - INTEGRATION_GUIDE.md ## Files Modified - backend/app/main.py (added routers, static file serving) - backend/app/models/user.py (added devices relationship) - backend/.env.example (added device & mining variables) Tested locally with Docker Compose (PostgreSQL + Redis). Ready for Railway deployment.
This commit is contained in:
438
INTEGRATION_GUIDE.md
Normal file
438
INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
# BlackRoad OS Backend Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the complete integration between the BlackRoad OS desktop front-end and the FastAPI backend, transforming the static mockup into a fully functional web-based operating system.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Full-Stack Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ FRONT-END (BlackRoad OS Desktop UI) │
|
||||||
|
│ Location: backend/static/ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ index.html │ │
|
||||||
|
│ │ - Windows 95-inspired UI │ │
|
||||||
|
│ │ - 16 desktop applications │ │
|
||||||
|
│ │ - Dynamic content loading │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ JavaScript Modules │ │
|
||||||
|
│ │ ├─ api-client.js (API communication layer) │ │
|
||||||
|
│ │ ├─ auth.js (authentication & session management) │ │
|
||||||
|
│ │ └─ apps.js (application data loading & UI updates) │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↕ HTTP/JSON
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ BACK-END (FastAPI Server) │
|
||||||
|
│ Location: backend/app/ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ API Routers │ │
|
||||||
|
│ │ ├─ /api/auth - Authentication & user mgmt │ │
|
||||||
|
│ │ ├─ /api/blockchain - RoadCoin blockchain │ │
|
||||||
|
│ │ ├─ /api/miner - Mining stats & control │ │
|
||||||
|
│ │ ├─ /api/devices - IoT/Raspberry Pi management │ │
|
||||||
|
│ │ ├─ /api/email - RoadMail │ │
|
||||||
|
│ │ ├─ /api/social - Social media feed │ │
|
||||||
|
│ │ ├─ /api/videos - BlackStream video platform │ │
|
||||||
|
│ │ ├─ /api/files - File storage │ │
|
||||||
|
│ │ └─ /api/ai-chat - AI assistant │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ PostgreSQL │ │ Redis │ │
|
||||||
|
│ │ - All data models │ │ - Sessions/cache │ │
|
||||||
|
│ └────────────────────┘ └─────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## New Features Implemented
|
||||||
|
|
||||||
|
### 1. Device Management (Raspberry Pi / IoT)
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- New models: `Device`, `DeviceMetric`, `DeviceLog`
|
||||||
|
- Router: `backend/app/routers/devices.py`
|
||||||
|
- Endpoints:
|
||||||
|
- `GET /api/devices/` - List all devices
|
||||||
|
- `GET /api/devices/stats` - Overall device statistics
|
||||||
|
- `GET /api/devices/{device_id}` - Get device details
|
||||||
|
- `POST /api/devices/` - Register new device
|
||||||
|
- `PUT /api/devices/{device_id}` - Update device
|
||||||
|
- `POST /api/devices/{device_id}/heartbeat` - Device status update (for IoT agents)
|
||||||
|
- `DELETE /api/devices/{device_id}` - Remove device
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Window: Raspberry Pi (🥧)
|
||||||
|
- Shows online/offline status of all registered devices
|
||||||
|
- Displays CPU, RAM, temperature metrics for online devices
|
||||||
|
- Auto-populated from `/api/devices` endpoint
|
||||||
|
|
||||||
|
### 2. Mining Stats & Control (RoadCoin Miner)
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Router: `backend/app/routers/miner.py`
|
||||||
|
- Features:
|
||||||
|
- Simulated mining process with hashrate, shares, temperature
|
||||||
|
- Start/stop/restart mining controls
|
||||||
|
- Lifetime statistics (blocks mined, RoadCoins earned)
|
||||||
|
- Recent blocks listing
|
||||||
|
- Mining pool information
|
||||||
|
- Endpoints:
|
||||||
|
- `GET /api/miner/status` - Current miner performance
|
||||||
|
- `GET /api/miner/stats` - Lifetime mining statistics
|
||||||
|
- `GET /api/miner/blocks` - Recently mined blocks
|
||||||
|
- `POST /api/miner/control` - Start/stop mining
|
||||||
|
- `GET /api/miner/pool/info` - Pool connection info
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Window: RoadCoin Miner (⛏️)
|
||||||
|
- Live stats: hashrate, shares, temperature, power consumption
|
||||||
|
- Blocks mined count and RoadCoins earned
|
||||||
|
- Start/stop mining button
|
||||||
|
- Recent blocks list with timestamps
|
||||||
|
|
||||||
|
### 3. Enhanced Blockchain Explorer (RoadChain)
|
||||||
|
|
||||||
|
**Frontend Integration:**
|
||||||
|
- Window: RoadChain Explorer (⛓️)
|
||||||
|
- Live data from `/api/blockchain` endpoints:
|
||||||
|
- Chain height, total transactions, difficulty
|
||||||
|
- Recent blocks list (clickable for details)
|
||||||
|
- "Mine New Block" button
|
||||||
|
- Auto-refreshes blockchain stats
|
||||||
|
|
||||||
|
### 4. Live Wallet
|
||||||
|
|
||||||
|
**Frontend Integration:**
|
||||||
|
- Window: Wallet (💰)
|
||||||
|
- Real-time balance from `/api/blockchain/balance`
|
||||||
|
- Wallet address display with copy functionality
|
||||||
|
- Recent transactions list with incoming/outgoing indicators
|
||||||
|
- USD conversion estimate
|
||||||
|
|
||||||
|
### 5. Authentication System
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Login/Register modal that blocks access until authenticated
|
||||||
|
- JWT token-based authentication stored in `localStorage`
|
||||||
|
- Session persistence across page reloads
|
||||||
|
- Auto-logout on token expiration
|
||||||
|
- User-specific data isolation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `backend/static/js/auth.js` - Authentication module
|
||||||
|
- Automatic wallet creation on user registration
|
||||||
|
- Login form with keyboard support (Enter to submit)
|
||||||
|
|
||||||
|
### 6. Other Application Integrations
|
||||||
|
|
||||||
|
**RoadMail:**
|
||||||
|
- Connected to `/api/email` endpoints
|
||||||
|
- Shows real inbox messages
|
||||||
|
- Email detail viewing (TODO: full implementation)
|
||||||
|
|
||||||
|
**BlackRoad Social:**
|
||||||
|
- Connected to `/api/social/feed`
|
||||||
|
- Shows real posts from database
|
||||||
|
- Like/comment functionality
|
||||||
|
- Post creation (stub)
|
||||||
|
|
||||||
|
**BlackStream:**
|
||||||
|
- Connected to `/api/videos`
|
||||||
|
- Video grid with view/like counts
|
||||||
|
- Video playback (stub)
|
||||||
|
|
||||||
|
**AI Assistant:**
|
||||||
|
- Connected to `/api/ai-chat` endpoints
|
||||||
|
- Conversation management (basic UI)
|
||||||
|
- Message sending (simulated responses until OpenAI integration)
|
||||||
|
|
||||||
|
## API Client Architecture
|
||||||
|
|
||||||
|
### API Client Module (`api-client.js`)
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Automatic base URL detection (localhost vs production)
|
||||||
|
- JWT token management
|
||||||
|
- Automatic 401 handling (triggers re-login)
|
||||||
|
- Centralized error handling
|
||||||
|
- Type-safe method wrappers for all API endpoints
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
```javascript
|
||||||
|
// Get miner status
|
||||||
|
const status = await window.BlackRoadAPI.getMinerStatus();
|
||||||
|
|
||||||
|
// Mine a new block
|
||||||
|
const block = await window.BlackRoadAPI.mineBlock();
|
||||||
|
|
||||||
|
// Get devices
|
||||||
|
const devices = await window.BlackRoadAPI.getDevices();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apps Module (`apps.js`)
|
||||||
|
|
||||||
|
**Responsibilities:**
|
||||||
|
- Load data when windows are opened
|
||||||
|
- Auto-refresh for real-time windows (miner stats every 5s)
|
||||||
|
- Format and render dynamic content
|
||||||
|
- Handle user interactions (mine block, toggle miner, etc.)
|
||||||
|
|
||||||
|
**Window Loading:**
|
||||||
|
- Lazy loading: data is fetched only when window is opened
|
||||||
|
- Auto-refresh for critical apps (miner, blockchain)
|
||||||
|
- Efficient state management
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
**Required Environment Variables:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://user:pass@host:5432/db
|
||||||
|
DATABASE_ASYNC_URL=postgresql+asyncpg://user:pass@host:5432/db
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://host:6379/0
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECRET_KEY=your-production-secret-key
|
||||||
|
|
||||||
|
# CORS - Add your production domain
|
||||||
|
ALLOWED_ORIGINS=https://www.blackroad.systems
|
||||||
|
|
||||||
|
# Optional: AI Integration
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
|
||||||
|
# Optional: Email
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_USER=your-email@gmail.com
|
||||||
|
SMTP_PASSWORD=your-app-password
|
||||||
|
|
||||||
|
# Optional: File Storage
|
||||||
|
AWS_ACCESS_KEY_ID=...
|
||||||
|
AWS_SECRET_ACCESS_KEY=...
|
||||||
|
S3_BUCKET_NAME=blackroad-files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Railway Deployment
|
||||||
|
|
||||||
|
The app is designed to work seamlessly with Railway:
|
||||||
|
|
||||||
|
1. **Static Files**: Backend serves `backend/static/index.html` at root URL
|
||||||
|
2. **API Routes**: All API endpoints under `/api/*`
|
||||||
|
3. **CORS**: Configured to allow Railway domains
|
||||||
|
4. **Database**: PostgreSQL plugin
|
||||||
|
5. **Redis**: Redis plugin
|
||||||
|
|
||||||
|
**Start Command:**
|
||||||
|
```bash
|
||||||
|
cd backend && uvicorn app.main:app --host 0.0.0.0 --port $PORT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### New Tables
|
||||||
|
|
||||||
|
**devices:**
|
||||||
|
- Device registration and status tracking
|
||||||
|
- Real-time metrics (CPU, RAM, temperature)
|
||||||
|
- Services and capabilities tracking
|
||||||
|
- Owner association (user_id)
|
||||||
|
|
||||||
|
**device_metrics:**
|
||||||
|
- Time-series data for device performance
|
||||||
|
- Historical tracking
|
||||||
|
|
||||||
|
**device_logs:**
|
||||||
|
- Device event logging
|
||||||
|
- System, network, service, hardware events
|
||||||
|
|
||||||
|
### Updated Tables
|
||||||
|
|
||||||
|
**users:**
|
||||||
|
- Added `devices` relationship
|
||||||
|
|
||||||
|
## Static File Serving
|
||||||
|
|
||||||
|
The backend now serves the front-end:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# backend/app/main.py
|
||||||
|
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
||||||
|
app.mount("/static", StaticFiles(directory=static_dir, html=True), name="static")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def serve_frontend():
|
||||||
|
return FileResponse(os.path.join(static_dir, "index.html"))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
1. **Start Backend:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
docker-compose up -d # Start PostgreSQL + Redis
|
||||||
|
python run.py # Start FastAPI server on :8000
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Access UI:**
|
||||||
|
```
|
||||||
|
http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **API Docs:**
|
||||||
|
```
|
||||||
|
http://localhost:8000/api/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All static files are already in backend/static/
|
||||||
|
# No build step required - pure HTML/CSS/JS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Priority 1 (Core Functionality)
|
||||||
|
- [ ] Real XMRig integration for actual cryptocurrency mining
|
||||||
|
- [ ] WebSocket support for real-time updates
|
||||||
|
- [ ] MQTT broker integration for device heartbeats
|
||||||
|
- [ ] Actual AI chat integration (OpenAI/Anthropic API)
|
||||||
|
|
||||||
|
### Priority 2 (Features)
|
||||||
|
- [ ] File upload to S3
|
||||||
|
- [ ] Email sending via SMTP
|
||||||
|
- [ ] Video upload and streaming
|
||||||
|
- [ ] Enhanced blockchain features (peer-to-peer, consensus)
|
||||||
|
|
||||||
|
### Priority 3 (UX Improvements)
|
||||||
|
- [ ] Mobile responsive design
|
||||||
|
- [ ] Dark mode support
|
||||||
|
- [ ] Keyboard shortcuts for all actions
|
||||||
|
- [ ] Desktop icon customization
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
|
||||||
|
✅ **Implemented:**
|
||||||
|
- JWT authentication with token expiration
|
||||||
|
- Password hashing (bcrypt)
|
||||||
|
- CORS protection
|
||||||
|
- SQL injection prevention (SQLAlchemy ORM)
|
||||||
|
- User data isolation
|
||||||
|
|
||||||
|
⚠️ **TODO:**
|
||||||
|
- Rate limiting on API endpoints
|
||||||
|
- HTTPS enforcement in production
|
||||||
|
- Wallet private key encryption at rest
|
||||||
|
- Two-factor authentication
|
||||||
|
- API key rotation
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**1. Authentication Modal Won't Close**
|
||||||
|
- Check browser console for API errors
|
||||||
|
- Verify DATABASE_URL and SECRET_KEY are set
|
||||||
|
- Ensure PostgreSQL is running
|
||||||
|
|
||||||
|
**2. Static Files Not Loading**
|
||||||
|
- Verify `backend/static/` directory exists
|
||||||
|
- Check `backend/static/js/` has all three JS files
|
||||||
|
- Review browser console for 404 errors
|
||||||
|
|
||||||
|
**3. API Calls Failing**
|
||||||
|
- Check CORS settings in `.env`
|
||||||
|
- Verify ALLOWED_ORIGINS includes your domain
|
||||||
|
- Check browser network tab for CORS errors
|
||||||
|
|
||||||
|
**4. Mining Stats Not Updating**
|
||||||
|
- Verify user is logged in
|
||||||
|
- Check browser console for errors
|
||||||
|
- Ensure `/api/miner` endpoints are working (test in `/api/docs`)
|
||||||
|
|
||||||
|
## File Structure Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
BlackRoad-Operating-System/
|
||||||
|
├── backend/
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── main.py # FastAPI app with static file serving
|
||||||
|
│ │ ├── models/
|
||||||
|
│ │ │ ├── device.py # NEW: Device, DeviceMetric, DeviceLog
|
||||||
|
│ │ │ └── user.py # UPDATED: Added devices relationship
|
||||||
|
│ │ └── routers/
|
||||||
|
│ │ ├── devices.py # NEW: Device management API
|
||||||
|
│ │ └── miner.py # NEW: Mining stats & control API
|
||||||
|
│ ├── static/
|
||||||
|
│ │ ├── index.html # UPDATED: Added auth modal, CSS, JS imports
|
||||||
|
│ │ └── js/
|
||||||
|
│ │ ├── api-client.js # NEW: Centralized API client
|
||||||
|
│ │ ├── auth.js # NEW: Authentication module
|
||||||
|
│ │ └── apps.js # NEW: Application data loaders
|
||||||
|
│ └── .env.example # UPDATED: Added device & mining vars
|
||||||
|
└── INTEGRATION_GUIDE.md # THIS FILE
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoint Summary
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/auth/register` - Create account
|
||||||
|
- `POST /api/auth/login` - Get JWT token
|
||||||
|
- `GET /api/auth/me` - Get current user
|
||||||
|
- `POST /api/auth/logout` - Invalidate token
|
||||||
|
|
||||||
|
### Mining
|
||||||
|
- `GET /api/miner/status` - Current performance
|
||||||
|
- `GET /api/miner/stats` - Lifetime stats
|
||||||
|
- `GET /api/miner/blocks` - Recent blocks
|
||||||
|
- `POST /api/miner/control` - Start/stop
|
||||||
|
|
||||||
|
### Blockchain
|
||||||
|
- `GET /api/blockchain/wallet` - User wallet
|
||||||
|
- `GET /api/blockchain/balance` - Current balance
|
||||||
|
- `GET /api/blockchain/blocks` - Recent blocks
|
||||||
|
- `POST /api/blockchain/mine` - Mine new block
|
||||||
|
- `GET /api/blockchain/stats` - Chain statistics
|
||||||
|
|
||||||
|
### Devices
|
||||||
|
- `GET /api/devices/` - List devices
|
||||||
|
- `GET /api/devices/stats` - Statistics
|
||||||
|
- `POST /api/devices/` - Register device
|
||||||
|
- `POST /api/devices/{id}/heartbeat` - Update status
|
||||||
|
|
||||||
|
### Social/Email/Videos
|
||||||
|
- See existing API documentation at `/api/docs`
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues, questions, or contributions:
|
||||||
|
- GitHub Issues: https://github.com/blackboxprogramming/BlackRoad-Operating-System/issues
|
||||||
|
- Pull Requests: Always welcome!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2025-11-16
|
||||||
|
**Version:** 1.0.0 - Full Backend Integration
|
||||||
@@ -30,12 +30,25 @@ APP_VERSION=1.0.0
|
|||||||
DEBUG=True
|
DEBUG=True
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
|
|
||||||
# CORS
|
# CORS (add your production domains here)
|
||||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://blackboxprogramming.github.io
|
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://blackboxprogramming.github.io,https://www.blackroad.systems
|
||||||
|
|
||||||
# API Keys
|
# API Keys
|
||||||
OPENAI_API_KEY=your-openai-key-for-ai-chat
|
OPENAI_API_KEY=your-openai-key-for-ai-chat
|
||||||
|
|
||||||
# Blockchain
|
# Blockchain & Mining
|
||||||
BLOCKCHAIN_DIFFICULTY=4
|
BLOCKCHAIN_DIFFICULTY=4
|
||||||
MINING_REWARD=50
|
MINING_REWARD=50
|
||||||
|
ROADCHAIN_RPC_URL=http://localhost:8545
|
||||||
|
ROADCOIN_POOL_URL=pool.roadcoin.network:3333
|
||||||
|
ROADCOIN_WALLET_ADDRESS=auto-generated-per-user
|
||||||
|
|
||||||
|
# Device Management (IoT/Raspberry Pi)
|
||||||
|
MQTT_BROKER_URL=mqtt://localhost:1883
|
||||||
|
MQTT_USERNAME=blackroad
|
||||||
|
MQTT_PASSWORD=your-mqtt-password
|
||||||
|
DEVICE_HEARTBEAT_TIMEOUT_SECONDS=300
|
||||||
|
|
||||||
|
# Deployment
|
||||||
|
# Railway will automatically set PORT - use this for local development
|
||||||
|
PORT=8000
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
"""Main FastAPI application"""
|
"""Main FastAPI application"""
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse, FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import async_engine, Base
|
from app.database import async_engine, Base
|
||||||
from app.redis_client import close_redis
|
from app.redis_client import close_redis
|
||||||
from app.routers import auth, email, social, video, files, blockchain, ai_chat
|
from app.routers import auth, email, social, video, files, blockchain, ai_chat, devices, miner
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -92,9 +94,33 @@ app.include_router(video.router)
|
|||||||
app.include_router(files.router)
|
app.include_router(files.router)
|
||||||
app.include_router(blockchain.router)
|
app.include_router(blockchain.router)
|
||||||
app.include_router(ai_chat.router)
|
app.include_router(ai_chat.router)
|
||||||
|
app.include_router(devices.router)
|
||||||
|
app.include_router(miner.router)
|
||||||
|
|
||||||
|
|
||||||
# Root endpoint
|
# Static file serving for the BlackRoad OS front-end
|
||||||
|
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
||||||
|
if os.path.exists(static_dir):
|
||||||
|
# Mount static files (JS, CSS, images)
|
||||||
|
app.mount("/static", StaticFiles(directory=static_dir, html=True), name="static")
|
||||||
|
|
||||||
|
# Serve index.html at root
|
||||||
|
@app.get("/")
|
||||||
|
async def serve_frontend():
|
||||||
|
"""Serve the BlackRoad OS desktop interface"""
|
||||||
|
index_path = os.path.join(static_dir, "index.html")
|
||||||
|
if os.path.exists(index_path):
|
||||||
|
return FileResponse(index_path)
|
||||||
|
return {
|
||||||
|
"name": settings.APP_NAME,
|
||||||
|
"version": settings.APP_VERSION,
|
||||||
|
"environment": settings.ENVIRONMENT,
|
||||||
|
"docs": "/api/docs",
|
||||||
|
"status": "operational",
|
||||||
|
"note": "Front-end not found. API is operational."
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Fallback if static directory doesn't exist
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""Root endpoint"""
|
"""Root endpoint"""
|
||||||
@@ -103,7 +129,8 @@ async def root():
|
|||||||
"version": settings.APP_VERSION,
|
"version": settings.APP_VERSION,
|
||||||
"environment": settings.ENVIRONMENT,
|
"environment": settings.ENVIRONMENT,
|
||||||
"docs": "/api/docs",
|
"docs": "/api/docs",
|
||||||
"status": "operational"
|
"status": "operational",
|
||||||
|
"note": "API-only mode. Front-end not deployed."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -131,7 +158,9 @@ async def api_info():
|
|||||||
"videos": "/api/videos",
|
"videos": "/api/videos",
|
||||||
"files": "/api/files",
|
"files": "/api/files",
|
||||||
"blockchain": "/api/blockchain",
|
"blockchain": "/api/blockchain",
|
||||||
"ai_chat": "/api/ai-chat"
|
"ai_chat": "/api/ai-chat",
|
||||||
|
"devices": "/api/devices",
|
||||||
|
"miner": "/api/miner"
|
||||||
},
|
},
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"swagger": "/api/docs",
|
"swagger": "/api/docs",
|
||||||
|
|||||||
110
backend/app/models/device.py
Normal file
110
backend/app/models/device.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"""Device management models for IoT/Raspberry Pi integration."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, JSON, ForeignKey, Text
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Device(Base):
|
||||||
|
"""IoT Device model - Raspberry Pi, Jetson, etc."""
|
||||||
|
|
||||||
|
__tablename__ = "devices"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
device_id = Column(String(100), unique=True, index=True, nullable=False) # Unique device identifier
|
||||||
|
name = Column(String(200), nullable=False) # User-friendly name
|
||||||
|
device_type = Column(String(50), nullable=False) # pi5, pi400, jetson, etc.
|
||||||
|
|
||||||
|
# Connection info
|
||||||
|
ip_address = Column(String(45)) # IPv4 or IPv6
|
||||||
|
hostname = Column(String(255))
|
||||||
|
mac_address = Column(String(17))
|
||||||
|
|
||||||
|
# Status
|
||||||
|
is_online = Column(Boolean, default=False)
|
||||||
|
status = Column(String(50), default="offline") # online, offline, error, maintenance
|
||||||
|
last_seen = Column(DateTime)
|
||||||
|
|
||||||
|
# System info
|
||||||
|
os_version = Column(String(100))
|
||||||
|
kernel_version = Column(String(100))
|
||||||
|
uptime_seconds = Column(Integer, default=0)
|
||||||
|
|
||||||
|
# Hardware specs
|
||||||
|
cpu_model = Column(String(200))
|
||||||
|
cpu_cores = Column(Integer)
|
||||||
|
ram_total_mb = Column(Integer)
|
||||||
|
disk_total_gb = Column(Integer)
|
||||||
|
|
||||||
|
# Current metrics
|
||||||
|
cpu_usage_percent = Column(Float, default=0.0)
|
||||||
|
ram_usage_percent = Column(Float, default=0.0)
|
||||||
|
disk_usage_percent = Column(Float, default=0.0)
|
||||||
|
temperature_celsius = Column(Float)
|
||||||
|
|
||||||
|
# Services running
|
||||||
|
services = Column(JSON, default=list) # List of active services
|
||||||
|
|
||||||
|
# Capabilities
|
||||||
|
capabilities = Column(JSON, default=list) # mining, sensor, camera, etc.
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
location = Column(String(200)) # Physical location
|
||||||
|
description = Column(Text)
|
||||||
|
tags = Column(JSON, default=list)
|
||||||
|
|
||||||
|
# Ownership
|
||||||
|
owner_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
owner = relationship("User", back_populates="devices")
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relations
|
||||||
|
metrics = relationship("DeviceMetric", back_populates="device", cascade="all, delete-orphan")
|
||||||
|
logs = relationship("DeviceLog", back_populates="device", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceMetric(Base):
|
||||||
|
"""Time-series metrics for devices."""
|
||||||
|
|
||||||
|
__tablename__ = "device_metrics"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
device_id = Column(Integer, ForeignKey("devices.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
|
||||||
|
# Metric data
|
||||||
|
timestamp = Column(DateTime, default=datetime.utcnow, index=True)
|
||||||
|
cpu_usage = Column(Float)
|
||||||
|
ram_usage = Column(Float)
|
||||||
|
disk_usage = Column(Float)
|
||||||
|
temperature = Column(Float)
|
||||||
|
network_bytes_sent = Column(Integer)
|
||||||
|
network_bytes_received = Column(Integer)
|
||||||
|
|
||||||
|
# Custom metrics (JSON for flexibility)
|
||||||
|
custom_data = Column(JSON, default=dict)
|
||||||
|
|
||||||
|
# Relationship
|
||||||
|
device = relationship("Device", back_populates="metrics")
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceLog(Base):
|
||||||
|
"""Device event logs."""
|
||||||
|
|
||||||
|
__tablename__ = "device_logs"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
device_id = Column(Integer, ForeignKey("devices.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
|
||||||
|
# Log data
|
||||||
|
timestamp = Column(DateTime, default=datetime.utcnow, index=True)
|
||||||
|
level = Column(String(20), nullable=False) # info, warning, error, critical
|
||||||
|
category = Column(String(50)) # system, network, service, hardware
|
||||||
|
message = Column(Text, nullable=False)
|
||||||
|
details = Column(JSON, default=dict)
|
||||||
|
|
||||||
|
# Relationship
|
||||||
|
device = relationship("Device", back_populates="logs")
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"""User model"""
|
"""User model"""
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
|
||||||
@@ -30,5 +31,8 @@ class User(Base):
|
|||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
last_login = Column(DateTime(timezone=True))
|
last_login = Column(DateTime(timezone=True))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
devices = relationship("Device", back_populates="owner")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<User {self.username}>"
|
return f"<User {self.username}>"
|
||||||
|
|||||||
345
backend/app/routers/devices.py
Normal file
345
backend/app/routers/devices.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
"""Device management router - Raspberry Pi, Jetson, and other IoT devices."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models.device import Device, DeviceMetric, DeviceLog
|
||||||
|
from app.models.user import User
|
||||||
|
from app.routers.auth import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/devices", tags=["devices"])
|
||||||
|
|
||||||
|
|
||||||
|
# Schemas
|
||||||
|
class DeviceCreate(BaseModel):
|
||||||
|
"""Schema for creating a new device."""
|
||||||
|
|
||||||
|
device_id: str
|
||||||
|
name: str
|
||||||
|
device_type: str
|
||||||
|
ip_address: Optional[str] = None
|
||||||
|
hostname: Optional[str] = None
|
||||||
|
mac_address: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceUpdate(BaseModel):
|
||||||
|
"""Schema for updating device info."""
|
||||||
|
|
||||||
|
name: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceHeartbeat(BaseModel):
|
||||||
|
"""Schema for device heartbeat/status update."""
|
||||||
|
|
||||||
|
ip_address: Optional[str] = None
|
||||||
|
hostname: Optional[str] = None
|
||||||
|
os_version: Optional[str] = None
|
||||||
|
kernel_version: Optional[str] = None
|
||||||
|
uptime_seconds: Optional[int] = None
|
||||||
|
cpu_model: Optional[str] = None
|
||||||
|
cpu_cores: Optional[int] = None
|
||||||
|
ram_total_mb: Optional[int] = None
|
||||||
|
disk_total_gb: Optional[int] = None
|
||||||
|
cpu_usage_percent: Optional[float] = None
|
||||||
|
ram_usage_percent: Optional[float] = None
|
||||||
|
disk_usage_percent: Optional[float] = None
|
||||||
|
temperature_celsius: Optional[float] = None
|
||||||
|
services: Optional[List[str]] = None
|
||||||
|
capabilities: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceResponse(BaseModel):
|
||||||
|
"""Schema for device response."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
device_id: str
|
||||||
|
name: str
|
||||||
|
device_type: str
|
||||||
|
ip_address: Optional[str]
|
||||||
|
hostname: Optional[str]
|
||||||
|
is_online: bool
|
||||||
|
status: str
|
||||||
|
last_seen: Optional[datetime]
|
||||||
|
cpu_usage_percent: Optional[float]
|
||||||
|
ram_usage_percent: Optional[float]
|
||||||
|
disk_usage_percent: Optional[float]
|
||||||
|
temperature_celsius: Optional[float]
|
||||||
|
uptime_seconds: Optional[int]
|
||||||
|
services: List[str]
|
||||||
|
capabilities: List[str]
|
||||||
|
location: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceStats(BaseModel):
|
||||||
|
"""Overall device statistics."""
|
||||||
|
|
||||||
|
total_devices: int
|
||||||
|
online_devices: int
|
||||||
|
offline_devices: int
|
||||||
|
total_cpu_usage: float
|
||||||
|
total_ram_usage: float
|
||||||
|
average_temperature: float
|
||||||
|
|
||||||
|
|
||||||
|
# Routes
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[DeviceResponse])
|
||||||
|
async def list_devices(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""List all devices for the current user."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Device)
|
||||||
|
.filter(Device.owner_id == current_user.id)
|
||||||
|
.offset(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.order_by(Device.created_at.desc())
|
||||||
|
)
|
||||||
|
devices = result.scalars().all()
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats", response_model=DeviceStats)
|
||||||
|
async def get_device_stats(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get overall device statistics."""
|
||||||
|
# Get counts
|
||||||
|
total_result = await db.execute(
|
||||||
|
select(func.count(Device.id)).filter(Device.owner_id == current_user.id)
|
||||||
|
)
|
||||||
|
total_devices = total_result.scalar() or 0
|
||||||
|
|
||||||
|
online_result = await db.execute(
|
||||||
|
select(func.count(Device.id)).filter(
|
||||||
|
Device.owner_id == current_user.id, Device.is_online == True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
online_devices = online_result.scalar() or 0
|
||||||
|
|
||||||
|
# Get average metrics for online devices
|
||||||
|
metrics_result = await db.execute(
|
||||||
|
select(
|
||||||
|
func.avg(Device.cpu_usage_percent),
|
||||||
|
func.avg(Device.ram_usage_percent),
|
||||||
|
func.avg(Device.temperature_celsius),
|
||||||
|
).filter(Device.owner_id == current_user.id, Device.is_online == True)
|
||||||
|
)
|
||||||
|
metrics = metrics_result.first()
|
||||||
|
|
||||||
|
return DeviceStats(
|
||||||
|
total_devices=total_devices,
|
||||||
|
online_devices=online_devices,
|
||||||
|
offline_devices=total_devices - online_devices,
|
||||||
|
total_cpu_usage=round(metrics[0] or 0.0, 2),
|
||||||
|
total_ram_usage=round(metrics[1] or 0.0, 2),
|
||||||
|
average_temperature=round(metrics[2] or 0.0, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{device_id}", response_model=DeviceResponse)
|
||||||
|
async def get_device(
|
||||||
|
device_id: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get device by ID."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Device).filter(
|
||||||
|
Device.device_id == device_id, Device.owner_id == current_user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
raise HTTPException(status_code=404, detail="Device not found")
|
||||||
|
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=DeviceResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_device(
|
||||||
|
device_data: DeviceCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Register a new device."""
|
||||||
|
# Check if device already exists
|
||||||
|
existing = await db.execute(
|
||||||
|
select(Device).filter(Device.device_id == device_data.device_id)
|
||||||
|
)
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Device with this ID already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
device = Device(
|
||||||
|
device_id=device_data.device_id,
|
||||||
|
name=device_data.name,
|
||||||
|
device_type=device_data.device_type,
|
||||||
|
ip_address=device_data.ip_address,
|
||||||
|
hostname=device_data.hostname,
|
||||||
|
mac_address=device_data.mac_address,
|
||||||
|
location=device_data.location,
|
||||||
|
description=device_data.description,
|
||||||
|
owner_id=current_user.id,
|
||||||
|
is_online=False,
|
||||||
|
status="offline",
|
||||||
|
services=[],
|
||||||
|
capabilities=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(device)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(device)
|
||||||
|
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{device_id}", response_model=DeviceResponse)
|
||||||
|
async def update_device(
|
||||||
|
device_id: str,
|
||||||
|
device_data: DeviceUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Update device information."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Device).filter(
|
||||||
|
Device.device_id == device_id, Device.owner_id == current_user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
raise HTTPException(status_code=404, detail="Device not found")
|
||||||
|
|
||||||
|
# Update fields
|
||||||
|
if device_data.name is not None:
|
||||||
|
device.name = device_data.name
|
||||||
|
if device_data.location is not None:
|
||||||
|
device.location = device_data.location
|
||||||
|
if device_data.description is not None:
|
||||||
|
device.description = device_data.description
|
||||||
|
|
||||||
|
device.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(device)
|
||||||
|
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{device_id}/heartbeat", response_model=DeviceResponse)
|
||||||
|
async def device_heartbeat(
|
||||||
|
device_id: str,
|
||||||
|
heartbeat_data: DeviceHeartbeat,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Receive device heartbeat and update status (public endpoint for devices)."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Device).filter(Device.device_id == device_id)
|
||||||
|
)
|
||||||
|
device = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
raise HTTPException(status_code=404, detail="Device not found")
|
||||||
|
|
||||||
|
# Update device status
|
||||||
|
device.is_online = True
|
||||||
|
device.status = "online"
|
||||||
|
device.last_seen = datetime.utcnow()
|
||||||
|
|
||||||
|
# Update system info if provided
|
||||||
|
if heartbeat_data.ip_address:
|
||||||
|
device.ip_address = heartbeat_data.ip_address
|
||||||
|
if heartbeat_data.hostname:
|
||||||
|
device.hostname = heartbeat_data.hostname
|
||||||
|
if heartbeat_data.os_version:
|
||||||
|
device.os_version = heartbeat_data.os_version
|
||||||
|
if heartbeat_data.kernel_version:
|
||||||
|
device.kernel_version = heartbeat_data.kernel_version
|
||||||
|
if heartbeat_data.uptime_seconds is not None:
|
||||||
|
device.uptime_seconds = heartbeat_data.uptime_seconds
|
||||||
|
|
||||||
|
# Update hardware info
|
||||||
|
if heartbeat_data.cpu_model:
|
||||||
|
device.cpu_model = heartbeat_data.cpu_model
|
||||||
|
if heartbeat_data.cpu_cores:
|
||||||
|
device.cpu_cores = heartbeat_data.cpu_cores
|
||||||
|
if heartbeat_data.ram_total_mb:
|
||||||
|
device.ram_total_mb = heartbeat_data.ram_total_mb
|
||||||
|
if heartbeat_data.disk_total_gb:
|
||||||
|
device.disk_total_gb = heartbeat_data.disk_total_gb
|
||||||
|
|
||||||
|
# Update current metrics
|
||||||
|
if heartbeat_data.cpu_usage_percent is not None:
|
||||||
|
device.cpu_usage_percent = heartbeat_data.cpu_usage_percent
|
||||||
|
if heartbeat_data.ram_usage_percent is not None:
|
||||||
|
device.ram_usage_percent = heartbeat_data.ram_usage_percent
|
||||||
|
if heartbeat_data.disk_usage_percent is not None:
|
||||||
|
device.disk_usage_percent = heartbeat_data.disk_usage_percent
|
||||||
|
if heartbeat_data.temperature_celsius is not None:
|
||||||
|
device.temperature_celsius = heartbeat_data.temperature_celsius
|
||||||
|
|
||||||
|
# Update services and capabilities
|
||||||
|
if heartbeat_data.services is not None:
|
||||||
|
device.services = heartbeat_data.services
|
||||||
|
if heartbeat_data.capabilities is not None:
|
||||||
|
device.capabilities = heartbeat_data.capabilities
|
||||||
|
|
||||||
|
# Save metric snapshot
|
||||||
|
metric = DeviceMetric(
|
||||||
|
device_id=device.id,
|
||||||
|
timestamp=datetime.utcnow(),
|
||||||
|
cpu_usage=heartbeat_data.cpu_usage_percent,
|
||||||
|
ram_usage=heartbeat_data.ram_usage_percent,
|
||||||
|
disk_usage=heartbeat_data.disk_usage_percent,
|
||||||
|
temperature=heartbeat_data.temperature_celsius,
|
||||||
|
)
|
||||||
|
db.add(metric)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(device)
|
||||||
|
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_device(
|
||||||
|
device_id: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Delete a device."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Device).filter(
|
||||||
|
Device.device_id == device_id, Device.owner_id == current_user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
raise HTTPException(status_code=404, detail="Device not found")
|
||||||
|
|
||||||
|
await db.delete(device)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
303
backend/app/routers/miner.py
Normal file
303
backend/app/routers/miner.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
"""Mining statistics and control router - RoadCoin Miner integration."""
|
||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Optional
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, func, desc
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models.blockchain import Block, Wallet
|
||||||
|
from app.models.user import User
|
||||||
|
from app.routers.auth import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/miner", tags=["miner"])
|
||||||
|
|
||||||
|
|
||||||
|
# In-memory miner state (for simulation)
|
||||||
|
class MinerState:
|
||||||
|
"""Global miner state."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.is_mining = False
|
||||||
|
self.hashrate_mhs = 0.0
|
||||||
|
self.shares_submitted = 0
|
||||||
|
self.shares_accepted = 0
|
||||||
|
self.pool_url = "pool.roadcoin.network:3333"
|
||||||
|
self.worker_id = "RoadMiner-1"
|
||||||
|
self.started_at: Optional[datetime] = None
|
||||||
|
self.temperature_celsius = 65.0
|
||||||
|
self.power_watts = 120.0
|
||||||
|
|
||||||
|
|
||||||
|
miner_state = MinerState()
|
||||||
|
|
||||||
|
|
||||||
|
# Schemas
|
||||||
|
class MinerStatus(BaseModel):
|
||||||
|
"""Current miner status."""
|
||||||
|
|
||||||
|
is_mining: bool
|
||||||
|
hashrate_mhs: float
|
||||||
|
shares_submitted: int
|
||||||
|
shares_accepted: int
|
||||||
|
shares_rejected: int
|
||||||
|
pool_url: str
|
||||||
|
worker_id: str
|
||||||
|
uptime_seconds: int
|
||||||
|
temperature_celsius: float
|
||||||
|
power_watts: float
|
||||||
|
efficiency_mhs_per_watt: float
|
||||||
|
|
||||||
|
|
||||||
|
class MinerStats(BaseModel):
|
||||||
|
"""Miner statistics."""
|
||||||
|
|
||||||
|
blocks_mined: int
|
||||||
|
roadcoins_earned: float
|
||||||
|
current_hashrate_mhs: float
|
||||||
|
average_hashrate_mhs: float
|
||||||
|
total_shares: int
|
||||||
|
accepted_shares: int
|
||||||
|
rejected_shares: int
|
||||||
|
last_block_time: Optional[datetime]
|
||||||
|
mining_since: Optional[datetime]
|
||||||
|
|
||||||
|
|
||||||
|
class MinerControl(BaseModel):
|
||||||
|
"""Miner control commands."""
|
||||||
|
|
||||||
|
action: str # start, stop, restart
|
||||||
|
pool_url: Optional[str] = None
|
||||||
|
worker_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RecentBlock(BaseModel):
|
||||||
|
"""Recent mined block info."""
|
||||||
|
|
||||||
|
block_index: int
|
||||||
|
block_hash: str
|
||||||
|
reward: float
|
||||||
|
timestamp: datetime
|
||||||
|
difficulty: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# Routes
|
||||||
|
|
||||||
|
@router.get("/status", response_model=MinerStatus)
|
||||||
|
async def get_miner_status(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get current miner status and performance metrics."""
|
||||||
|
uptime_seconds = 0
|
||||||
|
if miner_state.started_at:
|
||||||
|
uptime_seconds = int((datetime.utcnow() - miner_state.started_at).total_seconds())
|
||||||
|
|
||||||
|
# Simulate some variance in hashrate
|
||||||
|
current_hashrate = miner_state.hashrate_mhs
|
||||||
|
if miner_state.is_mining:
|
||||||
|
current_hashrate = miner_state.hashrate_mhs + random.uniform(-2.0, 2.0)
|
||||||
|
|
||||||
|
# Calculate efficiency
|
||||||
|
efficiency = 0.0
|
||||||
|
if miner_state.power_watts > 0:
|
||||||
|
efficiency = current_hashrate / miner_state.power_watts
|
||||||
|
|
||||||
|
rejected_shares = miner_state.shares_submitted - miner_state.shares_accepted
|
||||||
|
|
||||||
|
return MinerStatus(
|
||||||
|
is_mining=miner_state.is_mining,
|
||||||
|
hashrate_mhs=round(current_hashrate, 2),
|
||||||
|
shares_submitted=miner_state.shares_submitted,
|
||||||
|
shares_accepted=miner_state.shares_accepted,
|
||||||
|
shares_rejected=rejected_shares,
|
||||||
|
pool_url=miner_state.pool_url,
|
||||||
|
worker_id=miner_state.worker_id,
|
||||||
|
uptime_seconds=uptime_seconds,
|
||||||
|
temperature_celsius=miner_state.temperature_celsius,
|
||||||
|
power_watts=miner_state.power_watts,
|
||||||
|
efficiency_mhs_per_watt=round(efficiency, 4),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats", response_model=MinerStats)
|
||||||
|
async def get_miner_stats(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get overall mining statistics."""
|
||||||
|
# Get user's wallet
|
||||||
|
wallet_result = await db.execute(
|
||||||
|
select(Wallet).filter(Wallet.user_id == current_user.id)
|
||||||
|
)
|
||||||
|
wallet = wallet_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
# Count blocks mined by this user
|
||||||
|
blocks_count_result = await db.execute(
|
||||||
|
select(func.count(Block.id)).filter(Block.miner == wallet.address if wallet else None)
|
||||||
|
)
|
||||||
|
blocks_mined = blocks_count_result.scalar() or 0
|
||||||
|
|
||||||
|
# Sum rewards earned
|
||||||
|
rewards_result = await db.execute(
|
||||||
|
select(func.sum(Block.reward)).filter(Block.miner == wallet.address if wallet else None)
|
||||||
|
)
|
||||||
|
roadcoins_earned = rewards_result.scalar() or 0.0
|
||||||
|
|
||||||
|
# Get last block mined
|
||||||
|
last_block_result = await db.execute(
|
||||||
|
select(Block)
|
||||||
|
.filter(Block.miner == wallet.address if wallet else None)
|
||||||
|
.order_by(desc(Block.timestamp))
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
last_block = last_block_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
# Calculate average hashrate (simulated based on blocks mined)
|
||||||
|
average_hashrate = 0.0
|
||||||
|
if blocks_mined > 0:
|
||||||
|
# Rough estimate: difficulty 4 = ~40 MH/s average
|
||||||
|
average_hashrate = 40.0 + (blocks_mined * 0.5)
|
||||||
|
|
||||||
|
return MinerStats(
|
||||||
|
blocks_mined=blocks_mined,
|
||||||
|
roadcoins_earned=float(roadcoins_earned),
|
||||||
|
current_hashrate_mhs=miner_state.hashrate_mhs if miner_state.is_mining else 0.0,
|
||||||
|
average_hashrate_mhs=round(average_hashrate, 2),
|
||||||
|
total_shares=miner_state.shares_submitted,
|
||||||
|
accepted_shares=miner_state.shares_accepted,
|
||||||
|
rejected_shares=miner_state.shares_submitted - miner_state.shares_accepted,
|
||||||
|
last_block_time=last_block.timestamp if last_block else None,
|
||||||
|
mining_since=miner_state.started_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/blocks", response_model=List[RecentBlock])
|
||||||
|
async def get_recent_blocks(
|
||||||
|
limit: int = 10,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get recently mined blocks by this user."""
|
||||||
|
# Get user's wallet
|
||||||
|
wallet_result = await db.execute(
|
||||||
|
select(Wallet).filter(Wallet.user_id == current_user.id)
|
||||||
|
)
|
||||||
|
wallet = wallet_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not wallet:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Get recent blocks
|
||||||
|
blocks_result = await db.execute(
|
||||||
|
select(Block)
|
||||||
|
.filter(Block.miner == wallet.address)
|
||||||
|
.order_by(desc(Block.timestamp))
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
blocks = blocks_result.scalars().all()
|
||||||
|
|
||||||
|
return [
|
||||||
|
RecentBlock(
|
||||||
|
block_index=block.index,
|
||||||
|
block_hash=block.hash,
|
||||||
|
reward=block.reward,
|
||||||
|
timestamp=block.timestamp,
|
||||||
|
difficulty=block.difficulty,
|
||||||
|
)
|
||||||
|
for block in blocks
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/control")
|
||||||
|
async def control_miner(
|
||||||
|
control: MinerControl,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Control miner operations (start/stop/restart)."""
|
||||||
|
if control.action == "start":
|
||||||
|
if miner_state.is_mining:
|
||||||
|
raise HTTPException(status_code=400, detail="Miner is already running")
|
||||||
|
|
||||||
|
miner_state.is_mining = True
|
||||||
|
miner_state.started_at = datetime.utcnow()
|
||||||
|
miner_state.hashrate_mhs = random.uniform(38.0, 45.0) # Simulate hashrate
|
||||||
|
|
||||||
|
if control.pool_url:
|
||||||
|
miner_state.pool_url = control.pool_url
|
||||||
|
if control.worker_id:
|
||||||
|
miner_state.worker_id = control.worker_id
|
||||||
|
|
||||||
|
# Start background mining simulation
|
||||||
|
background_tasks.add_task(simulate_mining)
|
||||||
|
|
||||||
|
return {"message": "Miner started successfully", "status": "running"}
|
||||||
|
|
||||||
|
elif control.action == "stop":
|
||||||
|
if not miner_state.is_mining:
|
||||||
|
raise HTTPException(status_code=400, detail="Miner is not running")
|
||||||
|
|
||||||
|
miner_state.is_mining = False
|
||||||
|
miner_state.hashrate_mhs = 0.0
|
||||||
|
|
||||||
|
return {"message": "Miner stopped successfully", "status": "stopped"}
|
||||||
|
|
||||||
|
elif control.action == "restart":
|
||||||
|
miner_state.is_mining = False
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
miner_state.is_mining = True
|
||||||
|
miner_state.started_at = datetime.utcnow()
|
||||||
|
miner_state.hashrate_mhs = random.uniform(38.0, 45.0)
|
||||||
|
|
||||||
|
background_tasks.add_task(simulate_mining)
|
||||||
|
|
||||||
|
return {"message": "Miner restarted successfully", "status": "running"}
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid action: {control.action}")
|
||||||
|
|
||||||
|
|
||||||
|
async def simulate_mining():
|
||||||
|
"""Background task to simulate mining activity."""
|
||||||
|
while miner_state.is_mining:
|
||||||
|
# Simulate share submission every 10-30 seconds
|
||||||
|
await asyncio.sleep(random.uniform(10, 30))
|
||||||
|
|
||||||
|
if not miner_state.is_mining:
|
||||||
|
break
|
||||||
|
|
||||||
|
miner_state.shares_submitted += 1
|
||||||
|
|
||||||
|
# 95% acceptance rate
|
||||||
|
if random.random() < 0.95:
|
||||||
|
miner_state.shares_accepted += 1
|
||||||
|
|
||||||
|
# Vary hashrate slightly
|
||||||
|
miner_state.hashrate_mhs = random.uniform(38.0, 45.0)
|
||||||
|
|
||||||
|
# Vary temperature
|
||||||
|
miner_state.temperature_celsius = random.uniform(60.0, 75.0)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/pool/info")
|
||||||
|
async def get_pool_info(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get mining pool information."""
|
||||||
|
return {
|
||||||
|
"pool_url": miner_state.pool_url,
|
||||||
|
"pool_name": "RoadCoin Mining Pool",
|
||||||
|
"pool_hashrate": "2.4 GH/s",
|
||||||
|
"connected_miners": 142,
|
||||||
|
"pool_fee": "1%",
|
||||||
|
"min_payout": 10.0,
|
||||||
|
"payment_interval_hours": 24,
|
||||||
|
"last_block_found": (datetime.utcnow() - timedelta(minutes=random.randint(5, 120))).isoformat(),
|
||||||
|
}
|
||||||
2202
backend/static/index.html
Normal file
2202
backend/static/index.html
Normal file
File diff suppressed because it is too large
Load Diff
409
backend/static/js/api-client.js
Normal file
409
backend/static/js/api-client.js
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
/**
|
||||||
|
* BlackRoad OS API Client
|
||||||
|
* Centralized API communication module
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
constructor() {
|
||||||
|
// Determine API base URL based on environment
|
||||||
|
this.baseUrl = this.getApiBaseUrl();
|
||||||
|
this.token = localStorage.getItem('blackroad_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API base URL (development vs production)
|
||||||
|
*/
|
||||||
|
getApiBaseUrl() {
|
||||||
|
// In production on Railway, API and front-end are served from same origin
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
// Local development - backend on port 8000
|
||||||
|
return 'http://localhost:8000';
|
||||||
|
} else if (hostname === 'www.blackroad.systems' || hostname.includes('railway.app')) {
|
||||||
|
// Production - same origin
|
||||||
|
return window.location.origin;
|
||||||
|
} else {
|
||||||
|
// Default to same origin
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set authentication token
|
||||||
|
*/
|
||||||
|
setToken(token) {
|
||||||
|
this.token = token;
|
||||||
|
localStorage.setItem('blackroad_token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear authentication token
|
||||||
|
*/
|
||||||
|
clearToken() {
|
||||||
|
this.token = null;
|
||||||
|
localStorage.removeItem('blackroad_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current token
|
||||||
|
*/
|
||||||
|
getToken() {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
*/
|
||||||
|
isAuthenticated() {
|
||||||
|
return !!this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request headers
|
||||||
|
*/
|
||||||
|
getHeaders(includeAuth = true) {
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeAuth && this.token) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make API request
|
||||||
|
*/
|
||||||
|
async request(method, endpoint, data = null, options = {}) {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
const config = {
|
||||||
|
method,
|
||||||
|
headers: this.getHeaders(options.includeAuth !== false),
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
config.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, config);
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized
|
||||||
|
if (response.status === 401) {
|
||||||
|
this.clearToken();
|
||||||
|
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||||
|
throw new Error('Session expired. Please log in again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-2xx responses
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 204 No Content
|
||||||
|
if (response.status === 204) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`API request failed: ${method} ${endpoint}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET request
|
||||||
|
*/
|
||||||
|
async get(endpoint, options = {}) {
|
||||||
|
return this.request('GET', endpoint, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request
|
||||||
|
*/
|
||||||
|
async post(endpoint, data = null, options = {}) {
|
||||||
|
return this.request('POST', endpoint, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT request
|
||||||
|
*/
|
||||||
|
async put(endpoint, data = null, options = {}) {
|
||||||
|
return this.request('PUT', endpoint, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE request
|
||||||
|
*/
|
||||||
|
async delete(endpoint, options = {}) {
|
||||||
|
return this.request('DELETE', endpoint, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Authentication API =====
|
||||||
|
|
||||||
|
async register(username, email, password, fullName = null) {
|
||||||
|
return this.post('/api/auth/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
full_name: fullName
|
||||||
|
}, { includeAuth: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username, password) {
|
||||||
|
const response = await this.post('/api/auth/login', {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}, { includeAuth: false });
|
||||||
|
|
||||||
|
if (response.access_token) {
|
||||||
|
this.setToken(response.access_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
await this.post('/api/auth/logout');
|
||||||
|
} finally {
|
||||||
|
this.clearToken();
|
||||||
|
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentUser() {
|
||||||
|
return this.get('/api/auth/me');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Blockchain/Wallet API =====
|
||||||
|
|
||||||
|
async getWallet() {
|
||||||
|
return this.get('/api/blockchain/wallet');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBalance() {
|
||||||
|
return this.get('/api/blockchain/balance');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTransactions(limit = 10, offset = 0) {
|
||||||
|
return this.get(`/api/blockchain/transactions?limit=${limit}&offset=${offset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTransaction(txHash) {
|
||||||
|
return this.get(`/api/blockchain/transactions/${txHash}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTransaction(toAddress, amount) {
|
||||||
|
return this.post('/api/blockchain/transactions', {
|
||||||
|
to_address: toAddress,
|
||||||
|
amount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlocks(limit = 10, offset = 0) {
|
||||||
|
return this.get(`/api/blockchain/blocks?limit=${limit}&offset=${offset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlock(blockId) {
|
||||||
|
return this.get(`/api/blockchain/blocks/${blockId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mineBlock() {
|
||||||
|
return this.post('/api/blockchain/mine');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlockchainStats() {
|
||||||
|
return this.get('/api/blockchain/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Miner API =====
|
||||||
|
|
||||||
|
async getMinerStatus() {
|
||||||
|
return this.get('/api/miner/status');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMinerStats() {
|
||||||
|
return this.get('/api/miner/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMinedBlocks(limit = 10) {
|
||||||
|
return this.get(`/api/miner/blocks?limit=${limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async controlMiner(action, poolUrl = null, workerId = null) {
|
||||||
|
return this.post('/api/miner/control', {
|
||||||
|
action,
|
||||||
|
pool_url: poolUrl,
|
||||||
|
worker_id: workerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPoolInfo() {
|
||||||
|
return this.get('/api/miner/pool/info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Devices API =====
|
||||||
|
|
||||||
|
async getDevices() {
|
||||||
|
return this.get('/api/devices/');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeviceStats() {
|
||||||
|
return this.get('/api/devices/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDevice(deviceId) {
|
||||||
|
return this.get(`/api/devices/${deviceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDevice(deviceData) {
|
||||||
|
return this.post('/api/devices/', deviceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDevice(deviceId, deviceData) {
|
||||||
|
return this.put(`/api/devices/${deviceId}`, deviceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDevice(deviceId) {
|
||||||
|
return this.delete(`/api/devices/${deviceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Email API =====
|
||||||
|
|
||||||
|
async getEmailFolders() {
|
||||||
|
return this.get('/api/email/folders');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmails(folder = 'inbox', limit = 50, offset = 0) {
|
||||||
|
const endpoint = folder === 'inbox'
|
||||||
|
? `/api/email/inbox?limit=${limit}&offset=${offset}`
|
||||||
|
: `/api/email/sent?limit=${limit}&offset=${offset}`;
|
||||||
|
return this.get(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmail(emailId) {
|
||||||
|
return this.get(`/api/email/${emailId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEmail(to, subject, body, cc = null, bcc = null) {
|
||||||
|
return this.post('/api/email/send', {
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
body,
|
||||||
|
cc,
|
||||||
|
bcc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEmail(emailId) {
|
||||||
|
return this.delete(`/api/email/${emailId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Social API =====
|
||||||
|
|
||||||
|
async getSocialFeed(limit = 20, offset = 0) {
|
||||||
|
return this.get(`/api/social/feed?limit=${limit}&offset=${offset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPost(content, images = null, videos = null) {
|
||||||
|
return this.post('/api/social/posts', {
|
||||||
|
content,
|
||||||
|
images,
|
||||||
|
videos
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async likePost(postId) {
|
||||||
|
return this.post(`/api/social/posts/${postId}/like`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getComments(postId) {
|
||||||
|
return this.get(`/api/social/posts/${postId}/comments`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addComment(postId, content) {
|
||||||
|
return this.post(`/api/social/posts/${postId}/comments`, {
|
||||||
|
content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async followUser(userId) {
|
||||||
|
return this.post(`/api/social/users/${userId}/follow`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Video API =====
|
||||||
|
|
||||||
|
async getVideos(limit = 20, offset = 0) {
|
||||||
|
return this.get(`/api/videos?limit=${limit}&offset=${offset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVideo(videoId) {
|
||||||
|
return this.get(`/api/videos/${videoId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async likeVideo(videoId) {
|
||||||
|
return this.post(`/api/videos/${videoId}/like`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== AI Chat API =====
|
||||||
|
|
||||||
|
async getConversations() {
|
||||||
|
return this.get('/api/ai-chat/conversations');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConversation(title = 'New Conversation') {
|
||||||
|
return this.post('/api/ai-chat/conversations', { title });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConversation(conversationId) {
|
||||||
|
return this.get(`/api/ai-chat/conversations/${conversationId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMessages(conversationId) {
|
||||||
|
return this.get(`/api/ai-chat/conversations/${conversationId}/messages`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(conversationId, message) {
|
||||||
|
return this.post(`/api/ai-chat/conversations/${conversationId}/messages`, {
|
||||||
|
message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteConversation(conversationId) {
|
||||||
|
return this.delete(`/api/ai-chat/conversations/${conversationId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Files API =====
|
||||||
|
|
||||||
|
async getFolders() {
|
||||||
|
return this.get('/api/files/folders');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFiles(folderId = null) {
|
||||||
|
const endpoint = folderId
|
||||||
|
? `/api/files?folder_id=${folderId}`
|
||||||
|
: '/api/files';
|
||||||
|
return this.get(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFile(fileId) {
|
||||||
|
return this.get(`/api/files/${fileId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(fileId) {
|
||||||
|
return this.delete(`/api/files/${fileId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
const api = new ApiClient();
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
window.BlackRoadAPI = api;
|
||||||
630
backend/static/js/apps.js
Normal file
630
backend/static/js/apps.js
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
/**
|
||||||
|
* BlackRoad OS Application Modules
|
||||||
|
* Handles data loading and UI updates for all desktop applications
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BlackRoadApps {
|
||||||
|
constructor() {
|
||||||
|
this.api = window.BlackRoadAPI;
|
||||||
|
this.refreshIntervals = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all apps when user logs in
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
// Listen for login event
|
||||||
|
window.addEventListener('auth:login', () => {
|
||||||
|
this.loadAllApps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for window open events to load data on-demand
|
||||||
|
this.setupWindowListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all apps data
|
||||||
|
*/
|
||||||
|
async loadAllApps() {
|
||||||
|
// Load critical apps immediately
|
||||||
|
await Promise.all([
|
||||||
|
this.loadWallet(),
|
||||||
|
this.loadMinerStats(),
|
||||||
|
this.loadBlockchainStats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Load other apps in the background
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadDevices();
|
||||||
|
this.loadEmailInbox();
|
||||||
|
this.loadSocialFeed();
|
||||||
|
this.loadVideos();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup listeners for window open events
|
||||||
|
*/
|
||||||
|
setupWindowListeners() {
|
||||||
|
// Override the global openWindow function to load data when windows open
|
||||||
|
const originalOpenWindow = window.openWindow;
|
||||||
|
window.openWindow = (id) => {
|
||||||
|
originalOpenWindow(id);
|
||||||
|
this.onWindowOpened(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle window opened event
|
||||||
|
*/
|
||||||
|
onWindowOpened(windowId) {
|
||||||
|
switch (windowId) {
|
||||||
|
case 'roadcoin-miner':
|
||||||
|
this.loadMinerStatus();
|
||||||
|
this.startMinerRefresh();
|
||||||
|
break;
|
||||||
|
case 'roadchain':
|
||||||
|
this.loadBlockchainExplorer();
|
||||||
|
break;
|
||||||
|
case 'wallet':
|
||||||
|
this.loadWallet();
|
||||||
|
break;
|
||||||
|
case 'raspberry-pi':
|
||||||
|
this.loadDevices();
|
||||||
|
break;
|
||||||
|
case 'roadmail':
|
||||||
|
this.loadEmailInbox();
|
||||||
|
break;
|
||||||
|
case 'blackroad-social':
|
||||||
|
this.loadSocialFeed();
|
||||||
|
break;
|
||||||
|
case 'blackstream':
|
||||||
|
this.loadVideos();
|
||||||
|
break;
|
||||||
|
case 'ai-chat':
|
||||||
|
this.loadAIChat();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start auto-refresh for a window
|
||||||
|
*/
|
||||||
|
startRefresh(windowId, callback, interval = 5000) {
|
||||||
|
this.stopRefresh(windowId);
|
||||||
|
this.refreshIntervals[windowId] = setInterval(callback, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop auto-refresh for a window
|
||||||
|
*/
|
||||||
|
stopRefresh(windowId) {
|
||||||
|
if (this.refreshIntervals[windowId]) {
|
||||||
|
clearInterval(this.refreshIntervals[windowId]);
|
||||||
|
delete this.refreshIntervals[windowId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== MINER APPLICATION =====
|
||||||
|
|
||||||
|
async loadMinerStatus() {
|
||||||
|
try {
|
||||||
|
const [status, stats, blocks] = await Promise.all([
|
||||||
|
this.api.getMinerStatus(),
|
||||||
|
this.api.getMinerStats(),
|
||||||
|
this.api.getMinedBlocks(5),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.updateMinerUI(status, stats, blocks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load miner status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMinerStats() {
|
||||||
|
try {
|
||||||
|
const stats = await this.api.getMinerStats();
|
||||||
|
this.updateMinerStatsInTaskbar(stats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load miner stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMinerUI(status, stats, blocks) {
|
||||||
|
const content = document.querySelector('#roadcoin-miner .window-content');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
const statusColor = status.is_mining ? '#2ecc40' : '#ff4136';
|
||||||
|
const statusText = status.is_mining ? 'MINING' : 'STOPPED';
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="miner-dashboard">
|
||||||
|
<div class="miner-header">
|
||||||
|
<h2>⛏️ RoadCoin Miner</h2>
|
||||||
|
<div class="miner-status" style="color: ${statusColor}; font-weight: bold;">
|
||||||
|
${statusText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="miner-stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Hashrate</div>
|
||||||
|
<div class="stat-value">${status.hashrate_mhs.toFixed(2)} MH/s</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Shares</div>
|
||||||
|
<div class="stat-value">${status.shares_accepted}/${status.shares_submitted}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Temperature</div>
|
||||||
|
<div class="stat-value">${status.temperature_celsius.toFixed(1)}°C</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Power</div>
|
||||||
|
<div class="stat-value">${status.power_watts.toFixed(0)}W</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="miner-lifetime-stats">
|
||||||
|
<h3>Lifetime Statistics</h3>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>Blocks Mined:</span>
|
||||||
|
<span><strong>${stats.blocks_mined}</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>RoadCoins Earned:</span>
|
||||||
|
<span><strong>${stats.roadcoins_earned.toFixed(2)} RC</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>Pool:</span>
|
||||||
|
<span>${status.pool_url}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="miner-recent-blocks">
|
||||||
|
<h3>Recent Blocks</h3>
|
||||||
|
<div class="blocks-list">
|
||||||
|
${blocks.length > 0 ? blocks.map(block => `
|
||||||
|
<div class="block-item">
|
||||||
|
<span>Block #${block.block_index}</span>
|
||||||
|
<span>${block.reward.toFixed(2)} RC</span>
|
||||||
|
<span class="text-muted">${this.formatTime(block.timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
`).join('') : '<div class="text-muted">No blocks mined yet</div>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="miner-controls">
|
||||||
|
<button class="btn ${status.is_mining ? 'btn-danger' : 'btn-success'}"
|
||||||
|
onclick="window.BlackRoadApps.toggleMiner()">
|
||||||
|
${status.is_mining ? 'Stop Mining' : 'Start Mining'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMinerStatsInTaskbar(stats) {
|
||||||
|
// Update system tray icon tooltip or status
|
||||||
|
const trayIcon = document.querySelector('.system-tray span:last-child');
|
||||||
|
if (trayIcon) {
|
||||||
|
trayIcon.title = `Mining: ${stats.blocks_mined} blocks, ${stats.roadcoins_earned.toFixed(2)} RC earned`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleMiner() {
|
||||||
|
try {
|
||||||
|
const status = await this.api.getMinerStatus();
|
||||||
|
const action = status.is_mining ? 'stop' : 'start';
|
||||||
|
await this.api.controlMiner(action);
|
||||||
|
await this.loadMinerStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle miner:', error);
|
||||||
|
alert('Failed to control miner: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startMinerRefresh() {
|
||||||
|
this.startRefresh('roadcoin-miner', () => this.loadMinerStatus(), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== BLOCKCHAIN EXPLORER =====
|
||||||
|
|
||||||
|
async loadBlockchainExplorer() {
|
||||||
|
try {
|
||||||
|
const [stats, blocks] = await Promise.all([
|
||||||
|
this.api.getBlockchainStats(),
|
||||||
|
this.api.getBlocks(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.updateBlockchainUI(stats, blocks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load blockchain data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBlockchainStats() {
|
||||||
|
try {
|
||||||
|
const stats = await this.api.getBlockchainStats();
|
||||||
|
this.updateBlockchainStatsInTaskbar(stats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load blockchain stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBlockchainUI(stats, blocks) {
|
||||||
|
const content = document.querySelector('#roadchain .window-content');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="blockchain-explorer">
|
||||||
|
<div class="explorer-header">
|
||||||
|
<h2>⛓️ RoadChain Explorer</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blockchain-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Chain Height</div>
|
||||||
|
<div class="stat-value">${stats.total_blocks}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Transactions</div>
|
||||||
|
<div class="stat-value">${stats.total_transactions}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Difficulty</div>
|
||||||
|
<div class="stat-value">${stats.difficulty}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recent-blocks">
|
||||||
|
<h3>Recent Blocks</h3>
|
||||||
|
<div class="blocks-table">
|
||||||
|
${blocks.map(block => `
|
||||||
|
<div class="block-row" onclick="window.BlackRoadApps.showBlockDetail(${block.id})">
|
||||||
|
<div class="block-index">#${block.index}</div>
|
||||||
|
<div class="block-hash">${block.hash.substring(0, 16)}...</div>
|
||||||
|
<div class="block-txs">${block.transactions?.length || 0} txs</div>
|
||||||
|
<div class="block-time">${this.formatTime(block.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="explorer-actions">
|
||||||
|
<button class="btn btn-primary" onclick="window.BlackRoadApps.mineNewBlock()">
|
||||||
|
Mine New Block
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBlockchainStatsInTaskbar(stats) {
|
||||||
|
// Could update a taskbar indicator
|
||||||
|
}
|
||||||
|
|
||||||
|
async mineNewBlock() {
|
||||||
|
try {
|
||||||
|
const result = await this.api.mineBlock();
|
||||||
|
alert(`Successfully mined block #${result.index}! Reward: ${result.reward} RC`);
|
||||||
|
await this.loadBlockchainExplorer();
|
||||||
|
await this.loadWallet();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mine block:', error);
|
||||||
|
alert('Failed to mine block: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showBlockDetail(blockId) {
|
||||||
|
// TODO: Open block detail modal
|
||||||
|
console.log('Show block detail:', blockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== WALLET =====
|
||||||
|
|
||||||
|
async loadWallet() {
|
||||||
|
try {
|
||||||
|
const [wallet, balance, transactions] = await Promise.all([
|
||||||
|
this.api.getWallet(),
|
||||||
|
this.api.getBalance(),
|
||||||
|
this.api.getTransactions(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.updateWalletUI(wallet, balance, transactions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load wallet:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWalletUI(wallet, balance, transactions) {
|
||||||
|
const content = document.querySelector('#wallet .window-content');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
const usdValue = balance.balance * 15; // Mock conversion rate
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="wallet-container">
|
||||||
|
<div class="wallet-header">
|
||||||
|
<h2>💰 RoadCoin Wallet</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wallet-balance">
|
||||||
|
<div class="balance-amount">${balance.balance.toFixed(8)} RC</div>
|
||||||
|
<div class="balance-usd">≈ $${usdValue.toFixed(2)} USD</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wallet-address">
|
||||||
|
<label>Your Address:</label>
|
||||||
|
<div class="address-field">
|
||||||
|
<input type="text" readonly value="${wallet.address}"
|
||||||
|
onclick="this.select()" style="width: 100%; font-size: 9px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wallet-transactions">
|
||||||
|
<h3>Recent Transactions</h3>
|
||||||
|
<div class="transactions-list">
|
||||||
|
${transactions.length > 0 ? transactions.map(tx => {
|
||||||
|
const isReceived = tx.to_address === wallet.address;
|
||||||
|
const sign = isReceived ? '+' : '-';
|
||||||
|
const color = isReceived ? '#2ecc40' : '#ff4136';
|
||||||
|
return `
|
||||||
|
<div class="transaction-item">
|
||||||
|
<div class="tx-type" style="color: ${color};">${sign}${tx.amount.toFixed(4)} RC</div>
|
||||||
|
<div class="tx-hash">${tx.hash.substring(0, 12)}...</div>
|
||||||
|
<div class="tx-time">${this.formatTime(tx.created_at)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('') : '<div class="text-muted">No transactions yet</div>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== DEVICES (RASPBERRY PI) =====
|
||||||
|
|
||||||
|
async loadDevices() {
|
||||||
|
try {
|
||||||
|
const [devices, stats] = await Promise.all([
|
||||||
|
this.api.getDevices(),
|
||||||
|
this.api.getDeviceStats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.updateDevicesUI(devices, stats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load devices:', error);
|
||||||
|
// Show stub UI if no devices yet
|
||||||
|
this.updateDevicesUI([], {
|
||||||
|
total_devices: 0,
|
||||||
|
online_devices: 0,
|
||||||
|
offline_devices: 0,
|
||||||
|
total_cpu_usage: 0,
|
||||||
|
total_ram_usage: 0,
|
||||||
|
average_temperature: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDevicesUI(devices, stats) {
|
||||||
|
const content = document.querySelector('#raspberry-pi .window-content');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="devices-container">
|
||||||
|
<div class="devices-header">
|
||||||
|
<h2>🥧 Device Manager</h2>
|
||||||
|
<div class="devices-stats">
|
||||||
|
<span class="text-success">${stats.online_devices} online</span> /
|
||||||
|
<span class="text-muted">${stats.total_devices} total</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="devices-list">
|
||||||
|
${devices.length > 0 ? devices.map(device => {
|
||||||
|
const statusColor = device.is_online ? '#2ecc40' : '#aaa';
|
||||||
|
const statusText = device.is_online ? '🟢 Online' : '🔴 Offline';
|
||||||
|
return `
|
||||||
|
<div class="device-card">
|
||||||
|
<div class="device-name">
|
||||||
|
<strong>${device.name}</strong>
|
||||||
|
<span class="device-type">${device.device_type}</span>
|
||||||
|
</div>
|
||||||
|
<div class="device-status" style="color: ${statusColor};">
|
||||||
|
${statusText}
|
||||||
|
</div>
|
||||||
|
${device.is_online ? `
|
||||||
|
<div class="device-metrics">
|
||||||
|
<div class="metric">CPU: ${device.cpu_usage_percent?.toFixed(1) || 0}%</div>
|
||||||
|
<div class="metric">RAM: ${device.ram_usage_percent?.toFixed(1) || 0}%</div>
|
||||||
|
<div class="metric">Temp: ${device.temperature_celsius?.toFixed(1) || 0}°C</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('') : `
|
||||||
|
<div class="no-devices">
|
||||||
|
<p>No devices registered yet.</p>
|
||||||
|
<p class="text-muted">Deploy a device agent to see your Raspberry Pi, Jetson, and other IoT devices here.</p>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== EMAIL =====
|
||||||
|
|
||||||
|
async loadEmailInbox() {
|
||||||
|
try {
|
||||||
|
const emails = await this.api.getEmails('inbox', 20);
|
||||||
|
this.updateEmailUI(emails);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load emails:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmailUI(emails) {
|
||||||
|
const emailList = document.querySelector('#roadmail .email-list');
|
||||||
|
if (!emailList) return;
|
||||||
|
|
||||||
|
if (emails.length === 0) {
|
||||||
|
emailList.innerHTML = '<div class="text-muted" style="padding: 10px;">No emails yet</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emailList.innerHTML = emails.map(email => `
|
||||||
|
<div class="email-item ${email.is_read ? '' : 'unread'}" onclick="window.BlackRoadApps.openEmail(${email.id})">
|
||||||
|
<div class="email-from">${email.sender || 'Unknown'}</div>
|
||||||
|
<div class="email-subject">${email.subject}</div>
|
||||||
|
<div class="email-date">${this.formatTime(email.created_at)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
openEmail(emailId) {
|
||||||
|
console.log('Open email:', emailId);
|
||||||
|
// TODO: Show email detail
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== SOCIAL FEED =====
|
||||||
|
|
||||||
|
async loadSocialFeed() {
|
||||||
|
try {
|
||||||
|
const feed = await this.api.getSocialFeed(20);
|
||||||
|
this.updateSocialUI(feed);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load social feed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSocialUI(posts) {
|
||||||
|
const feedContainer = document.querySelector('#blackroad-social .social-feed');
|
||||||
|
if (!feedContainer) return;
|
||||||
|
|
||||||
|
if (posts.length === 0) {
|
||||||
|
feedContainer.innerHTML = '<div class="text-muted">No posts yet. Be the first to post!</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
feedContainer.innerHTML = posts.map(post => `
|
||||||
|
<div class="post-card">
|
||||||
|
<div class="post-author">
|
||||||
|
<strong>${post.author?.username || 'Anonymous'}</strong>
|
||||||
|
<span class="text-muted">${this.formatTime(post.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="post-content">${post.content}</div>
|
||||||
|
<div class="post-actions">
|
||||||
|
<button class="btn-link" onclick="window.BlackRoadApps.likePost(${post.id})">
|
||||||
|
❤️ ${post.likes_count || 0}
|
||||||
|
</button>
|
||||||
|
<button class="btn-link">💬 ${post.comments_count || 0}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async likePost(postId) {
|
||||||
|
try {
|
||||||
|
await this.api.likePost(postId);
|
||||||
|
await this.loadSocialFeed();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to like post:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== VIDEOS =====
|
||||||
|
|
||||||
|
async loadVideos() {
|
||||||
|
try {
|
||||||
|
const videos = await this.api.getVideos(20);
|
||||||
|
this.updateVideosUI(videos);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load videos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVideosUI(videos) {
|
||||||
|
const videoGrid = document.querySelector('#blackstream .video-grid');
|
||||||
|
if (!videoGrid) return;
|
||||||
|
|
||||||
|
if (videos.length === 0) {
|
||||||
|
videoGrid.innerHTML = '<div class="text-muted">No videos available</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoGrid.innerHTML = videos.map(video => `
|
||||||
|
<div class="video-card" onclick="window.BlackRoadApps.playVideo(${video.id})">
|
||||||
|
<div class="video-thumbnail">📹</div>
|
||||||
|
<div class="video-title">${video.title}</div>
|
||||||
|
<div class="video-stats">
|
||||||
|
<span>👁️ ${video.views || 0}</span>
|
||||||
|
<span>❤️ ${video.likes || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
playVideo(videoId) {
|
||||||
|
console.log('Play video:', videoId);
|
||||||
|
// TODO: Open video player
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== AI CHAT =====
|
||||||
|
|
||||||
|
async loadAIChat() {
|
||||||
|
const content = document.querySelector('#ai-chat .window-content');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="ai-chat-container">
|
||||||
|
<div class="chat-messages" id="ai-chat-messages">
|
||||||
|
<div class="text-muted">AI Assistant ready! How can I help you?</div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-input">
|
||||||
|
<input type="text" id="ai-chat-input" placeholder="Type your message..." />
|
||||||
|
<button class="btn btn-primary" onclick="window.BlackRoadApps.sendAIMessage()">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAIMessage() {
|
||||||
|
const input = document.getElementById('ai-chat-input');
|
||||||
|
const message = input.value.trim();
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
console.log('Send AI message:', message);
|
||||||
|
// TODO: Implement AI chat
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== UTILITY FUNCTIONS =====
|
||||||
|
|
||||||
|
formatTime(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now - date;
|
||||||
|
|
||||||
|
if (diff < 60000) return 'Just now';
|
||||||
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
||||||
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
||||||
|
if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago`;
|
||||||
|
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
const blackRoadApps = new BlackRoadApps();
|
||||||
|
window.BlackRoadApps = blackRoadApps;
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
blackRoadApps.initialize();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
blackRoadApps.initialize();
|
||||||
|
}
|
||||||
303
backend/static/js/auth.js
Normal file
303
backend/static/js/auth.js
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/**
|
||||||
|
* BlackRoad OS Authentication Module
|
||||||
|
* Handles login, registration, and session management
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AuthManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentUser = null;
|
||||||
|
this.api = window.BlackRoadAPI;
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize authentication
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
// Listen for logout events
|
||||||
|
window.addEventListener('auth:logout', () => {
|
||||||
|
this.handleLogout();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if user is already logged in
|
||||||
|
if (this.api.isAuthenticated()) {
|
||||||
|
try {
|
||||||
|
await this.loadCurrentUser();
|
||||||
|
this.hideAuthModal();
|
||||||
|
window.dispatchEvent(new CustomEvent('auth:login', { detail: this.currentUser }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load current user:', error);
|
||||||
|
this.showAuthModal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show login modal if not authenticated
|
||||||
|
this.showAuthModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load current user data
|
||||||
|
*/
|
||||||
|
async loadCurrentUser() {
|
||||||
|
this.currentUser = await this.api.getCurrentUser();
|
||||||
|
return this.currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user
|
||||||
|
*/
|
||||||
|
getCurrentUser() {
|
||||||
|
return this.currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show authentication modal
|
||||||
|
*/
|
||||||
|
showAuthModal() {
|
||||||
|
const modal = document.getElementById('auth-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide authentication modal
|
||||||
|
*/
|
||||||
|
hideAuthModal() {
|
||||||
|
const modal = document.getElementById('auth-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle login
|
||||||
|
*/
|
||||||
|
async handleLogin(username, password) {
|
||||||
|
try {
|
||||||
|
const response = await this.api.login(username, password);
|
||||||
|
await this.loadCurrentUser();
|
||||||
|
this.hideAuthModal();
|
||||||
|
window.dispatchEvent(new CustomEvent('auth:login', { detail: this.currentUser }));
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle registration
|
||||||
|
*/
|
||||||
|
async handleRegister(username, email, password, fullName) {
|
||||||
|
try {
|
||||||
|
await this.api.register(username, email, password, fullName);
|
||||||
|
// Auto-login after registration
|
||||||
|
return await this.handleLogin(username, password);
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle logout
|
||||||
|
*/
|
||||||
|
async handleLogout() {
|
||||||
|
try {
|
||||||
|
await this.api.logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
this.currentUser = null;
|
||||||
|
this.showAuthModal();
|
||||||
|
// Optionally reload the page to reset state
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create auth modal HTML
|
||||||
|
*/
|
||||||
|
createAuthModal() {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.id = 'auth-modal';
|
||||||
|
modal.className = 'auth-modal';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="auth-modal-content">
|
||||||
|
<div class="auth-window">
|
||||||
|
<div class="title-bar">
|
||||||
|
<div class="title-text">
|
||||||
|
<span>🛣️</span>
|
||||||
|
<span>BlackRoad OS - Login</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-container">
|
||||||
|
<div class="auth-header">
|
||||||
|
<h1 style="font-size: 24px; margin-bottom: 10px;">Welcome to BlackRoad OS</h1>
|
||||||
|
<p style="color: #666; font-size: 11px;">Please login or register to continue</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<div id="login-form" class="auth-form">
|
||||||
|
<h3 style="margin-bottom: 15px;">Login</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username:</label>
|
||||||
|
<input type="text" id="login-username" class="form-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password:</label>
|
||||||
|
<input type="password" id="login-password" class="form-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-error" id="login-error"></div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" onclick="window.BlackRoadAuth.submitLogin()">Login</button>
|
||||||
|
<button class="btn btn-secondary" onclick="window.BlackRoadAuth.switchToRegister()">Register</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Register Form -->
|
||||||
|
<div id="register-form" class="auth-form" style="display: none;">
|
||||||
|
<h3 style="margin-bottom: 15px;">Create Account</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username:</label>
|
||||||
|
<input type="text" id="register-username" class="form-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email:</label>
|
||||||
|
<input type="email" id="register-email" class="form-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Full Name:</label>
|
||||||
|
<input type="text" id="register-fullname" class="form-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password:</label>
|
||||||
|
<input type="password" id="register-password" class="form-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Confirm Password:</label>
|
||||||
|
<input type="password" id="register-password2" class="form-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-error" id="register-error"></div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" onclick="window.BlackRoadAuth.submitRegister()">Create Account</button>
|
||||||
|
<button class="btn btn-secondary" onclick="window.BlackRoadAuth.switchToLogin()">Back to Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to register form
|
||||||
|
*/
|
||||||
|
switchToRegister() {
|
||||||
|
document.getElementById('login-form').style.display = 'none';
|
||||||
|
document.getElementById('register-form').style.display = 'block';
|
||||||
|
document.querySelector('.auth-window .title-text span:last-child').textContent = 'BlackRoad OS - Register';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to login form
|
||||||
|
*/
|
||||||
|
switchToLogin() {
|
||||||
|
document.getElementById('register-form').style.display = 'none';
|
||||||
|
document.getElementById('login-form').style.display = 'block';
|
||||||
|
document.querySelector('.auth-window .title-text span:last-child').textContent = 'BlackRoad OS - Login';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit login form
|
||||||
|
*/
|
||||||
|
async submitLogin() {
|
||||||
|
const username = document.getElementById('login-username').value.trim();
|
||||||
|
const password = document.getElementById('login-password').value;
|
||||||
|
const errorEl = document.getElementById('login-error');
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
errorEl.textContent = 'Please enter username and password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorEl.textContent = 'Logging in...';
|
||||||
|
const result = await this.handleLogin(username, password);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
errorEl.textContent = result.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit register form
|
||||||
|
*/
|
||||||
|
async submitRegister() {
|
||||||
|
const username = document.getElementById('register-username').value.trim();
|
||||||
|
const email = document.getElementById('register-email').value.trim();
|
||||||
|
const fullName = document.getElementById('register-fullname').value.trim();
|
||||||
|
const password = document.getElementById('register-password').value;
|
||||||
|
const password2 = document.getElementById('register-password2').value;
|
||||||
|
const errorEl = document.getElementById('register-error');
|
||||||
|
|
||||||
|
if (!username || !email || !password) {
|
||||||
|
errorEl.textContent = 'Please fill in all required fields';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== password2) {
|
||||||
|
errorEl.textContent = 'Passwords do not match';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
errorEl.textContent = 'Password must be at least 6 characters';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorEl.textContent = 'Creating account...';
|
||||||
|
const result = await this.handleRegister(username, email, password, fullName);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
errorEl.textContent = result.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Enter key support for forms
|
||||||
|
*/
|
||||||
|
setupKeyboardShortcuts() {
|
||||||
|
document.getElementById('login-password').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.submitLogin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('register-password2').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.submitRegister();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
const authManager = new AuthManager();
|
||||||
|
window.BlackRoadAuth = authManager;
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
authManager.createAuthModal();
|
||||||
|
authManager.setupKeyboardShortcuts();
|
||||||
|
authManager.initialize();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authManager.createAuthModal();
|
||||||
|
authManager.setupKeyboardShortcuts();
|
||||||
|
authManager.initialize();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user