This commit adds the complete Dangerous Pi web management interface with all MVP features implemented and tested locally. ## New Features ### Backend (Python + FastAPI) - Complete FastAPI backend with async support - 40+ API endpoints (Health, PM3, WiFi, Updates, UPS, BLE, Plugins) - 6 managers: Session, WiFi, Update, UPS, BLE, Plugin - SQLite database with sessions, config, history, crash reports - Server-Sent Events (SSE) for real-time notifications - Mock PM3 worker for development without hardware ### WiFi Manager - Interface detection (USB vs built-in) - Network scanning with signal strength - Mode switching (AP/Client/Dual/Auto/Off) - Network connection with password support - Hidden SSID and saved networks support - Static IP and DHCP configuration - 10 WiFi API endpoints ### Update Manager - GitHub releases API integration - Automatic periodic update checks - Semantic version comparison - Update download with progress tracking - SHA256 checksum verification - Automatic installation with backup and rollback - PM3 client rebuild after updates - 6 Update API endpoints ### UPS Manager - I2C battery monitoring (MAX17040-compatible) - Battery percentage, voltage, current tracking - Power source detection (AC/Battery) - Safe shutdown triggers at configurable thresholds - Event callbacks for battery warnings - SSE and BLE notification integration - 3 UPS API endpoints ### BLE Manager - Bluetooth Low Energy notification support - Auto-detects BLE capability - Multiple notification types (updates, battery, shutdown, etc.) - BLE advertising management - Device connection tracking - 4 BLE API endpoints ### Plugin Framework - Dynamic plugin loading/unloading - Plugin lifecycle management (load, enable, disable, unload) - Hook system for extensibility - JSON-based metadata - Example "Hello World" plugin included - 7 Plugin API endpoints ### Frontend (Remix.js + React) - Cyberpunk-themed responsive UI - Dashboard with system status - PM3 command interface with history - Settings page with WiFi and Update management - Command logs viewer - Theme toggle (Dark/Light/Auto) - Server-side rendering (SSR) - Mobile-first responsive design ### System Integration - Systemd service with security hardening - Automated install/uninstall scripts - Environment configuration template - Hardware access groups (i2c, bluetooth, gpio, dialout) - Pi-gen stage 04 integration for OS image building - Port conflict resolution with ttyd-bash - I2C interface auto-enable for UPS HAT ### Testing - test_backend.py - Backend API tests - test_ups.py - UPS manager tests - test_ble.py - BLE manager tests - test_plugins.py - Plugin manager tests - All tests passing locally ### Documentation - 12 comprehensive documentation files - claude.md - AI development guide - WIFI_MANAGER.md - WiFi management guide - UPDATE_MANAGER.md - Update system guide - PORT_CONFLICT.md - Port conflict resolution guide - MVP_COMPLETE.md - MVP implementation summary - PROJECT_STATUS.md - Project status and roadmap - systemd/README.md - Service management docs - pi-gen integration documentation ## Technical Details - ~5,000+ lines of backend code - 11 Python dependencies (smbus2 added for UPS) - FastAPI with async/await throughout - Type hints and docstrings on all functions - RESTful API design with SSE for notifications - Security hardening (non-root, protected dirs, resource limits) ## Next Steps - Deploy to Raspberry Pi Zero 2 W hardware - Test with real Proxmark3 device - Test UPS HAT integration - Test BLE on Pi hardware - Build custom OS image with pi-gen - Performance optimization for Pi Zero 2 W 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
217 lines
7.1 KiB
TypeScript
217 lines
7.1 KiB
TypeScript
import { Form } from "@remix-run/react";
|
|
import { useState } from "react";
|
|
|
|
interface ConnectDialogProps {
|
|
network: {
|
|
ssid: string;
|
|
encrypted: boolean;
|
|
signal_strength: number;
|
|
} | null;
|
|
onClose: () => void;
|
|
isSubmitting: boolean;
|
|
}
|
|
|
|
export default function ConnectDialog({ network, onClose, isSubmitting }: ConnectDialogProps) {
|
|
const [password, setPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [hidden, setHidden] = useState(false);
|
|
const [customSSID, setCustomSSID] = useState("");
|
|
|
|
if (!network && !hidden) return null;
|
|
|
|
const ssid = hidden ? customSSID : network?.ssid || "";
|
|
const needsPassword = hidden || (network?.encrypted ?? false);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: "fixed",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: "rgba(10, 14, 26, 0.9)",
|
|
backdropFilter: "blur(8px)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
zIndex: 200,
|
|
padding: "var(--space-4)",
|
|
}}
|
|
onClick={onClose}
|
|
>
|
|
<div
|
|
className="card"
|
|
style={{
|
|
maxWidth: "500px",
|
|
width: "100%",
|
|
margin: 0,
|
|
animation: "toast-in 250ms ease",
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<h3 className="card-title" style={{ marginBottom: "var(--space-4)" }}>
|
|
<span style={{ color: "var(--color-primary)" }}>📡</span>{" "}
|
|
Connect to WiFi
|
|
</h3>
|
|
|
|
<Form method="post">
|
|
<input type="hidden" name="_action" value="connect_network" />
|
|
|
|
{/* SSID Input (for hidden networks) */}
|
|
{!hidden ? (
|
|
<>
|
|
<div className="form-group">
|
|
<label className="label">Network</label>
|
|
<div
|
|
style={{
|
|
padding: "var(--space-3)",
|
|
background: "var(--color-bg)",
|
|
border: "1px solid var(--color-border)",
|
|
borderRadius: "var(--radius)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
}}
|
|
>
|
|
<div>
|
|
<div style={{ fontWeight: 600 }}>{network?.ssid}</div>
|
|
<div style={{ fontSize: "0.75rem", color: "var(--color-text-muted)" }}>
|
|
{network?.encrypted ? "🔒 Secured" : "Unsecured"}
|
|
{" • "}
|
|
{network?.signal_strength} dBm
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="ssid" value={network?.ssid || ""} />
|
|
</div>
|
|
|
|
<div style={{ textAlign: "center", marginBottom: "var(--space-4)" }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
style={{ fontSize: "0.75rem", padding: "var(--space-2) var(--space-3)" }}
|
|
onClick={() => setHidden(true)}
|
|
>
|
|
Connect to hidden network instead
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="form-group">
|
|
<label htmlFor="ssid" className="label">
|
|
Network Name (SSID)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="ssid"
|
|
name="ssid"
|
|
className="input"
|
|
placeholder="Enter network name"
|
|
value={customSSID}
|
|
onChange={(e) => setCustomSSID(e.target.value)}
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ textAlign: "center", marginBottom: "var(--space-4)" }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
style={{ fontSize: "0.75rem", padding: "var(--space-2) var(--space-3)" }}
|
|
onClick={() => setHidden(false)}
|
|
>
|
|
← Back to scanned networks
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Password Input */}
|
|
{needsPassword && (
|
|
<div className="form-group">
|
|
<label htmlFor="password" className="label">
|
|
Password
|
|
</label>
|
|
<div style={{ position: "relative" }}>
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
id="password"
|
|
name="password"
|
|
className="input"
|
|
placeholder="Enter network password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required={needsPassword}
|
|
autoComplete="off"
|
|
style={{ paddingRight: "3rem" }}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
style={{
|
|
position: "absolute",
|
|
right: "var(--space-3)",
|
|
top: "50%",
|
|
transform: "translateY(-50%)",
|
|
background: "transparent",
|
|
border: "none",
|
|
color: "var(--color-text-secondary)",
|
|
cursor: "pointer",
|
|
fontSize: "1.25rem",
|
|
}}
|
|
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
>
|
|
{showPassword ? "👁" : "👁🗨"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Hidden Network Checkbox */}
|
|
<input type="hidden" name="hidden" value={hidden ? "true" : "false"} />
|
|
|
|
{/* Actions */}
|
|
<div style={{ display: "flex", gap: "var(--space-2)", marginTop: "var(--space-6)" }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
style={{ flex: 1 }}
|
|
onClick={onClose}
|
|
disabled={isSubmitting}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary"
|
|
style={{ flex: 1 }}
|
|
disabled={isSubmitting || (!ssid || (needsPassword && !password))}
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<span className="spinner"></span>
|
|
<span>Connecting...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<span>✓</span>
|
|
<span>Connect</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</Form>
|
|
|
|
{needsPassword && (
|
|
<p className="text-muted" style={{ fontSize: "0.75rem", marginTop: "var(--space-4)", textAlign: "center" }}>
|
|
Your password will be securely stored
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|