mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 07:57:19 -05:00
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.
346 lines
10 KiB
Python
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
|