Files
pi-pm3/app/frontend/app/components/ConnectDialog.tsx
michael 1da6730735 Add Dangerous Pi MVP implementation - complete backend and system integration
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>
2025-11-26 08:11:36 -08:00

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>
);
}