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:
Claude
2025-11-16 07:19:45 +00:00
parent 556ff72fcf
commit 138d79a6e3
11 changed files with 4803 additions and 17 deletions

View 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

View 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(),
}