docs: add ESP32 and iOS agent architecture reference
- reference/esp32-agent/: DESIGN, HARDWARE, API_COMPAT, ARCHITECTURE, README - reference/ios-agent/: DESIGN, API_COMPAT, UI_DESIGN, ARCHITECTURE, README - Both mirror Android agent architecture, UI design, and heartbeat flow - Architecture-only (no code) — avoids submodule conflicts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
# iOS Agent — UI Design Specs
|
||||
|
||||
**Status**: DESIGN ONLY
|
||||
**Mirror**: Android Agent (`apps/android-agent/`)
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the UI design for the iOS WireGuard agent. It mirrors the Android agent's design, colors, layout, and user experience.
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Status Colors
|
||||
|
||||
| Name | Hex | RGB | Usage |
|
||||
|------|-----|-----|-------|
|
||||
| `status_disconnected` | `#FFB0B0B0` | (176, 176, 176) | Disconnected state |
|
||||
| `status_connecting` | `#FFFFD93D` | (255, 217, 61) | Connecting state |
|
||||
| `status_connected` | `#FF4CAF50` | (76, 175, 80) | Connected state |
|
||||
| `status_failed` | `#FFFF6B6B` | (255, 107, 107) | Failed state |
|
||||
|
||||
### UI Colors
|
||||
|
||||
| Name | Hex | RGB | Usage |
|
||||
|------|-----|-----|-------|
|
||||
| `card_bg` | `#FF1A1A2E` | (26, 26, 46) | Card background |
|
||||
| `card_bg_dark` | `#FF0F0F1A` | (15, 15, 26) | Badge background |
|
||||
| `text_primary` | `#FFFFFFFF` | (255, 255, 255) | Primary text |
|
||||
| `text_secondary` | `#FFB0B0B0` | (176, 176, 176) | Secondary text |
|
||||
| `accent_green` | `#FF4CAF50` | (76, 175, 80) | Accent/highlight |
|
||||
| `accent_blue` | `#FF2196F3` | (33, 150, 243) | Links/actions |
|
||||
| `border_color` | `#FF2A2A3E` | (42, 42, 62) | Card borders |
|
||||
|
||||
### Log Colors
|
||||
|
||||
| Level | Hex | RGB |
|
||||
|-------|-----|-----|
|
||||
| `DEBUG` | `#FF6B6B6B` | (107, 107, 107) |
|
||||
| `INFO` | `#FFB0B0B0` | (176, 176, 176) |
|
||||
| `WARN` | `#FFFFD93D` | (255, 217, 61) |
|
||||
| `ERROR` | `#FFFF6B6B` | (255, 107, 107) |
|
||||
|
||||
## Color Definitions (Swift)
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
// Status colors
|
||||
static let statusDisconnected = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
|
||||
static let statusConnecting = Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D
|
||||
static let statusConnected = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50
|
||||
static let statusFailed = Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B
|
||||
|
||||
// UI colors
|
||||
static let cardBackground = Color(red: 0.10, green: 0.10, blue: 0.18) // #1A1A2E
|
||||
static let cardBackgroundDark = Color(red: 0.06, green: 0.06, blue: 0.10) // #0F0F1A
|
||||
static let textPrimary = Color.white // #FFFFFF
|
||||
static let textSecondary = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
|
||||
static let accentGreen = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50
|
||||
static let accentBlue = Color(red: 0.13, green: 0.59, blue: 0.95) // #2196F3
|
||||
static let borderColor = Color(red: 0.16, green: 0.16, blue: 0.24) // #2A2A3E
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Structure
|
||||
|
||||
### Main Screen (ContentView)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ NexusGuard [⚙️ Settings]│
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ ● STATUS: Connected │ │
|
||||
│ │ │ │
|
||||
│ │ IP Address 10.172.21.2 │ │
|
||||
│ │ Admin Web https://... │ │
|
||||
│ │ Device ID abc-123-... │ │
|
||||
│ │ Handshake 14:32:05 │ │
|
||||
│ │ Allowed IPs 0.0.0.0/0 │ │
|
||||
│ │ Transport HTTPS │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ [Status] [Ports] [Log] │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Port Forwards: 2 │ │
|
||||
│ │ TCP :8080 → 10.172.21.2:80│ │
|
||||
│ │ UDP :53 → 10.172.21.2:53│ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ [Copy Log] [Clear Log] │ │
|
||||
│ │ │ │
|
||||
│ │ 14:32:05 [INFO] Agent... │ │
|
||||
│ │ 14:32:05 [INFO] Tunnel... │ │
|
||||
│ │ 14:32:05 [WARN] Retry... │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ ══════════════════ │ │
|
||||
│ │ VPN Toggle │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Settings Screen (SettingsView)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ← Settings │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Server URL │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ https://api-nexus.datadunia.com│ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ Registration Token │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ •••••••••••••••• │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ Auto-Start on Boot │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ [Toggle] │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Save Settings │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Export Config │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Import Config │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Paste Config │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Specs
|
||||
|
||||
### Status Card
|
||||
|
||||
```swift
|
||||
struct StatusCard: View {
|
||||
let state: AgentState
|
||||
let config: WireGuardConfig?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// Status indicator
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(statusColor)
|
||||
.frame(width: 12, height: 12)
|
||||
Text(statusText)
|
||||
.font(.headline)
|
||||
.foregroundColor(statusColor)
|
||||
}
|
||||
|
||||
// Info fields
|
||||
if state == .connected {
|
||||
InfoRow(label: "IP Address", value: config?.internalIp ?? "-")
|
||||
InfoRow(label: "Admin Web", value: "https://api-nexus.datadunia.com")
|
||||
InfoRow(label: "Device ID", value: config?.deviceId ?? "-")
|
||||
InfoRow(label: "Handshake", value: lastHandshake ?? "-")
|
||||
InfoRow(label: "Allowed IPs", value: config?.allowedIps ?? "-")
|
||||
InfoRow(label: "Transport", value: transport)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.cardBackground)
|
||||
.cornerRadius(12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.borderColor, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
switch state {
|
||||
case .disconnected: return .statusDisconnected
|
||||
case .connecting: return .statusConnecting
|
||||
case .connected: return .statusConnected
|
||||
case .failed: return .statusFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Info Row
|
||||
|
||||
```swift
|
||||
struct InfoRow: View {
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.textSecondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.textPrimary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Port Forward Row
|
||||
|
||||
```swift
|
||||
struct PortForwardRow: View {
|
||||
let forward: PortForward
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(forward.protocol.uppercased())
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.cardBackgroundDark)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.accentGreen)
|
||||
.cornerRadius(4)
|
||||
|
||||
Text(":\(forward.publicPort)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.textPrimary)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(forward.targetIp):\(forward.targetPort)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.textSecondary)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Log Entry
|
||||
|
||||
```swift
|
||||
struct LogEntryView: View {
|
||||
let entry: LogEntry
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
Text(entry.timestamp, style: .time)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundColor(entry.level.color)
|
||||
|
||||
Text("[\(entry.level.rawValue)]")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundColor(entry.level.color)
|
||||
|
||||
Text("\(entry.tag): \(entry.message)")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundColor(entry.level.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LogLevel {
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .debug: return Color(red: 0.42, green: 0.42, blue: 0.42) // #6B6B6B
|
||||
case .info: return Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0
|
||||
case .warn: return Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D
|
||||
case .error: return Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VPN Toggle
|
||||
|
||||
```swift
|
||||
struct VPNToggle: View {
|
||||
@Binding var isOn: Bool
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(isOn ? Color.accentGreen : Color.statusDisconnected)
|
||||
.frame(height: 50)
|
||||
.overlay(
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 40, height: 40)
|
||||
.offset(x: isOn ? 40 : -40)
|
||||
Spacer()
|
||||
}
|
||||
)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tab Layout
|
||||
|
||||
```swift
|
||||
struct TabBar: View {
|
||||
@Binding var selectedTab: Int
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
TabButton(title: "Status", isSelected: selectedTab == 0) {
|
||||
selectedTab = 0
|
||||
}
|
||||
TabButton(title: "Ports", isSelected: selectedTab == 1) {
|
||||
selectedTab = 1
|
||||
}
|
||||
TabButton(title: "Log", isSelected: selectedTab == 2) {
|
||||
selectedTab = 2
|
||||
}
|
||||
}
|
||||
.background(Color.cardBackground)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
struct TabButton: View {
|
||||
let title: String
|
||||
let isSelected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(isSelected ? .textPrimary : .textSecondary)
|
||||
.padding(.vertical, 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(isSelected ? Color.accentGreen : Color.clear)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Sizes
|
||||
|
||||
| Element | Size | Weight | Design |
|
||||
|---------|------|--------|--------|
|
||||
| Status text | 18pt | Semibold | Default |
|
||||
| Info label | 14pt | Regular | Default |
|
||||
| Info value | 14pt | Regular | Monospaced |
|
||||
| Tab text | 14pt | Medium | Default |
|
||||
| Log text | 12pt | Regular | Monospaced |
|
||||
| Badge text | 10pt | Bold | Default |
|
||||
|
||||
### Font Definitions
|
||||
|
||||
```swift
|
||||
extension Font {
|
||||
static let statusText = Font.headline
|
||||
static let infoLabel = Font.subheadline
|
||||
static let infoValue = Font.subheadline.monospaced()
|
||||
static let tabText = Font.subheadline.weight(.medium)
|
||||
static let logText = Font.caption.monospaced()
|
||||
static let badgeText = Font.caption2.bold()
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing
|
||||
|
||||
| Element | Value |
|
||||
|---------|-------|
|
||||
| Card padding | 16pt |
|
||||
| Card corner radius | 12pt |
|
||||
| Card border width | 1pt |
|
||||
| Row spacing | 12pt |
|
||||
| Badge padding | 8pt horizontal, 4pt vertical |
|
||||
| Badge corner radius | 4pt |
|
||||
| Button height | 50pt |
|
||||
| Button corner radius | 25pt |
|
||||
|
||||
## Animation
|
||||
|
||||
### Status Change
|
||||
|
||||
```swift
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
state = .connected
|
||||
}
|
||||
```
|
||||
|
||||
### Tab Switch
|
||||
|
||||
```swift
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
selectedTab = 1
|
||||
}
|
||||
```
|
||||
|
||||
### VPN Toggle
|
||||
|
||||
```swift
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
|
||||
isVPNOn.toggle()
|
||||
}
|
||||
```
|
||||
|
||||
## Haptic Feedback
|
||||
|
||||
```swift
|
||||
// Success
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
|
||||
// Error
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
|
||||
// Selection
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
iOS agent uses dark mode only (matches Android design). Force dark mode:
|
||||
|
||||
```swift
|
||||
@main
|
||||
struct NexusGuardApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Design
|
||||
|
||||
### iPhone SE
|
||||
|
||||
- Compact layout
|
||||
- Smaller font sizes
|
||||
- Reduced padding
|
||||
|
||||
### iPhone 14/15
|
||||
|
||||
- Standard layout
|
||||
- Default font sizes
|
||||
- Standard padding
|
||||
|
||||
### iPhone 14/15 Pro Max
|
||||
|
||||
- Expanded layout
|
||||
- Larger font sizes
|
||||
- Increased padding
|
||||
|
||||
## Accessibility
|
||||
|
||||
### VoiceOver
|
||||
|
||||
```swift
|
||||
Text("Connected")
|
||||
.accessibilityLabel("VPN Status: Connected")
|
||||
.accessibilityHint("Double tap to toggle VPN")
|
||||
|
||||
InfoRow(label: "IP Address", value: "10.172.21.2")
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("IP Address: 10.172.21.2")
|
||||
```
|
||||
|
||||
### Dynamic Type
|
||||
|
||||
```swift
|
||||
Text("Status")
|
||||
.font(.headline)
|
||||
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
|
||||
```
|
||||
|
||||
### Reduce Motion
|
||||
|
||||
```swift
|
||||
@Environment(\.accessibilityReduceMotion) var reduceMotion
|
||||
|
||||
withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.3)) {
|
||||
state = .connected
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user