Files
blackroad-operating-system/backend/app/routers/devices.py
Claude 138d79a6e3 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.
2025-11-16 07:19:45 +00:00

346 lines
10 KiB
Python

"""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