mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 03:57:13 -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
|
||||
ENVIRONMENT=development
|
||||
|
||||
# CORS
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://blackboxprogramming.github.io
|
||||
# CORS (add your production domains here)
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://blackboxprogramming.github.io,https://www.blackroad.systems
|
||||
|
||||
# API Keys
|
||||
OPENAI_API_KEY=your-openai-key-for-ai-chat
|
||||
|
||||
# Blockchain
|
||||
# Blockchain & Mining
|
||||
BLOCKCHAIN_DIFFICULTY=4
|
||||
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"""
|
||||
from fastapi import FastAPI, Request
|
||||
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
|
||||
import time
|
||||
import os
|
||||
|
||||
from app.config import settings
|
||||
from app.database import async_engine, Base
|
||||
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
|
||||
@@ -92,18 +94,43 @@ app.include_router(video.router)
|
||||
app.include_router(files.router)
|
||||
app.include_router(blockchain.router)
|
||||
app.include_router(ai_chat.router)
|
||||
app.include_router(devices.router)
|
||||
app.include_router(miner.router)
|
||||
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
# 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("/")
|
||||
async def root():
|
||||
"""Root endpoint"""
|
||||
return {
|
||||
"name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"environment": settings.ENVIRONMENT,
|
||||
"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",
|
||||
"files": "/api/files",
|
||||
"blockchain": "/api/blockchain",
|
||||
"ai_chat": "/api/ai-chat"
|
||||
"ai_chat": "/api/ai-chat",
|
||||
"devices": "/api/devices",
|
||||
"miner": "/api/miner"
|
||||
},
|
||||
"documentation": {
|
||||
"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"""
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
@@ -30,5 +31,8 @@ class User(Base):
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
last_login = Column(DateTime(timezone=True))
|
||||
|
||||
# Relationships
|
||||
devices = relationship("Device", back_populates="owner")
|
||||
|
||||
def __repr__(self):
|
||||
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