Initial commit: Jarvis iOS app
SwiftUI client for a self-hosted RSS news-correlation platform: signal feed, story detail, article reader, feed manager, and LAN⇄Tailscale connectivity. Project generated from project.yml via XcodeGen. Includes CI build matrix (macOS 14/15 × Debug/Release), issue templates, backlog, and API/backend handoff docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
101
Jarvis/Connectivity/VPNProvider.swift
Normal file
101
Jarvis/Connectivity/VPNProvider.swift
Normal file
@@ -0,0 +1,101 @@
|
||||
// VPNProvider.swift
|
||||
// Jarvis — remote-access providers. The app can't start another app's VPN tunnel
|
||||
// (iOS sandboxing), so the provider only tailors hints, help text, and an
|
||||
// "open the app" jump. Reachability + endpoint selection is provider-agnostic.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum VPNProvider: String, CaseIterable, Codable, Identifiable {
|
||||
case direct // LAN only, no VPN
|
||||
case tailscale
|
||||
case headscale // self-hosted control plane, Tailscale clients
|
||||
case netbird
|
||||
case zerotier
|
||||
case netmaker
|
||||
case wireguard
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .direct: return "Direct (LAN)"
|
||||
case .tailscale: return "Tailscale"
|
||||
case .headscale: return "Headscale"
|
||||
case .netbird: return "NetBird"
|
||||
case .zerotier: return "ZeroTier"
|
||||
case .netmaker: return "Netmaker"
|
||||
case .wireguard: return "WireGuard"
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a separate remote/VPN address is meaningful for this provider.
|
||||
var needsRemote: Bool { self != .direct }
|
||||
|
||||
/// Placeholder shown in the remote-address field.
|
||||
var remoteHint: String {
|
||||
switch self {
|
||||
case .direct: return "—"
|
||||
case .tailscale: return "jarvis.tailnet.ts.net:8080 or 100.x.y.z:8080"
|
||||
case .headscale: return "100.x.y.z:8080 (MagicDNS or tailnet IP)"
|
||||
case .netbird: return "100.x.y.z:8080"
|
||||
case .zerotier: return "10.x.x.x:8080 (managed IP)"
|
||||
case .netmaker: return "10.x.x.x:8080"
|
||||
case .wireguard: return "10.0.0.x:8080 (peer IP)"
|
||||
}
|
||||
}
|
||||
|
||||
var blurb: String {
|
||||
switch self {
|
||||
case .direct:
|
||||
return "Use the LAN address only. Best when the phone is always on the home network."
|
||||
case .tailscale:
|
||||
return "On your home wifi Jarvis uses the LAN address directly; away, it uses your Tailscale address. Make sure Tailscale is connected when remote."
|
||||
case .headscale:
|
||||
return "Self-hosted Tailscale control plane. Same client behaviour as Tailscale — keep the client connected when away."
|
||||
case .netbird:
|
||||
return "Open-source WireGuard mesh. Keep the NetBird app connected when off the LAN."
|
||||
case .zerotier:
|
||||
return "Managed network via ZeroTier. Keep the ZeroTier app joined to the network when remote."
|
||||
case .netmaker:
|
||||
return "Self-hosted WireGuard mesh. Keep your Netmaker client up when off the LAN."
|
||||
case .wireguard:
|
||||
return "Plain WireGuard tunnel. Enable the tunnel in the WireGuard app when remote."
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort URL scheme + App Store fallback used by "Open <provider>".
|
||||
/// iOS can't start the tunnel for us — this just brings the app forward.
|
||||
private var urlScheme: URL? {
|
||||
switch self {
|
||||
case .tailscale, .headscale: return URL(string: "tailscale://")
|
||||
case .zerotier: return URL(string: "zerotier://")
|
||||
case .wireguard: return URL(string: "wireguard://")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var appStoreURL: URL? {
|
||||
switch self {
|
||||
case .tailscale, .headscale: return URL(string: "https://apps.apple.com/app/id1470499037")
|
||||
case .zerotier: return URL(string: "https://apps.apple.com/app/id1084101492")
|
||||
case .wireguard: return URL(string: "https://apps.apple.com/app/id1441195209")
|
||||
case .netbird: return URL(string: "https://apps.apple.com/app/id6443155627")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var canOpenApp: Bool { urlScheme != nil || appStoreURL != nil }
|
||||
|
||||
/// Try the app's scheme; fall back to its App Store page.
|
||||
@MainActor
|
||||
func openApp() {
|
||||
let candidates = [urlScheme, appStoreURL].compactMap { $0 }
|
||||
func attempt(_ i: Int) {
|
||||
guard i < candidates.count else { return }
|
||||
UIApplication.shared.open(candidates[i]) { ok in
|
||||
if !ok { attempt(i + 1) }
|
||||
}
|
||||
}
|
||||
attempt(0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user