Files
datadunia 84689b208e 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>
2026-07-06 14:20:10 +07:00

507 lines
16 KiB
Markdown

# 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
}
```