mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Show computer IP when network is enabled
This commit is contained in:
parent
929808dbb2
commit
fc31cac55f
2 changed files with 198 additions and 12 deletions
92
VibeTunnel/Core/Utilities/NetworkUtility.swift
Normal file
92
VibeTunnel/Core/Utilities/NetworkUtility.swift
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
/// Utility for network-related operations
|
||||||
|
enum NetworkUtility {
|
||||||
|
/// Get the primary IPv4 address of the local machine
|
||||||
|
static func getLocalIPAddress() -> String? {
|
||||||
|
var address: String?
|
||||||
|
|
||||||
|
// Create a socket to determine the local IP address
|
||||||
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
||||||
|
|
||||||
|
guard getifaddrs(&ifaddr) == 0 else { return nil }
|
||||||
|
defer { freeifaddrs(ifaddr) }
|
||||||
|
|
||||||
|
var ptr = ifaddr
|
||||||
|
while ptr != nil {
|
||||||
|
defer { ptr = ptr?.pointee.ifa_next }
|
||||||
|
|
||||||
|
guard let interface = ptr?.pointee else { continue }
|
||||||
|
|
||||||
|
// Skip loopback addresses
|
||||||
|
if interface.ifa_flags & UInt32(IFF_LOOPBACK) != 0 { continue }
|
||||||
|
|
||||||
|
// Check for IPv4 interface
|
||||||
|
let addrFamily = interface.ifa_addr.pointee.sa_family
|
||||||
|
if addrFamily == UInt8(AF_INET) {
|
||||||
|
// Get interface name
|
||||||
|
let name = String(cString: interface.ifa_name)
|
||||||
|
|
||||||
|
// Prefer en0 (typically Wi-Fi on Mac) or en1 (sometimes Ethernet)
|
||||||
|
// But accept any non-loopback IPv4 address
|
||||||
|
if name.hasPrefix("en") {
|
||||||
|
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
if getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
|
||||||
|
&hostname, socklen_t(hostname.count),
|
||||||
|
nil, 0, NI_NUMERICHOST) == 0 {
|
||||||
|
let ipAddress = String(cString: hostname)
|
||||||
|
|
||||||
|
// Prefer addresses that look like local network addresses
|
||||||
|
if ipAddress.hasPrefix("192.168.") ||
|
||||||
|
ipAddress.hasPrefix("10.") ||
|
||||||
|
ipAddress.hasPrefix("172.") {
|
||||||
|
return ipAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store as fallback if we don't find a better one
|
||||||
|
if address == nil {
|
||||||
|
address = ipAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all IPv4 addresses
|
||||||
|
static func getAllIPAddresses() -> [String] {
|
||||||
|
var addresses: [String] = []
|
||||||
|
|
||||||
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
||||||
|
|
||||||
|
guard getifaddrs(&ifaddr) == 0 else { return addresses }
|
||||||
|
defer { freeifaddrs(ifaddr) }
|
||||||
|
|
||||||
|
var ptr = ifaddr
|
||||||
|
while ptr != nil {
|
||||||
|
defer { ptr = ptr?.pointee.ifa_next }
|
||||||
|
|
||||||
|
guard let interface = ptr?.pointee else { continue }
|
||||||
|
|
||||||
|
// Skip loopback addresses
|
||||||
|
if interface.ifa_flags & UInt32(IFF_LOOPBACK) != 0 { continue }
|
||||||
|
|
||||||
|
// Check for IPv4 interface
|
||||||
|
let addrFamily = interface.ifa_addr.pointee.sa_family
|
||||||
|
if addrFamily == UInt8(AF_INET) {
|
||||||
|
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
if getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
|
||||||
|
&hostname, socklen_t(hostname.count),
|
||||||
|
nil, 0, NI_NUMERICHOST) == 0 {
|
||||||
|
let ipAddress = String(cString: hostname)
|
||||||
|
addresses.append(ipAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -290,6 +290,7 @@ struct DashboardSettingsView: View {
|
||||||
@State private var serverErrorMessage = ""
|
@State private var serverErrorMessage = ""
|
||||||
@State private var isTokenRevealed = false
|
@State private var isTokenRevealed = false
|
||||||
@State private var maskedToken = ""
|
@State private var maskedToken = ""
|
||||||
|
@State private var localIPAddress: String?
|
||||||
|
|
||||||
private let dashboardKeychain = DashboardKeychain.shared
|
private let dashboardKeychain = DashboardKeychain.shared
|
||||||
private let ngrokService = NgrokService.shared
|
private let ngrokService = NgrokService.shared
|
||||||
|
|
@ -380,13 +381,15 @@ struct DashboardSettingsView: View {
|
||||||
"When password protection is enabled, localhost connections can still access without a password. For remote access, any username is accepted - only the password is verified."
|
"When password protection is enabled, localhost connections can still access without a password. For remote access, any username is accepted - only the password is verified."
|
||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
// Access Mode
|
// Access Mode
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Allow accessing dashboard:")
|
Text("Allow accessing the dashboard from:")
|
||||||
Spacer()
|
Spacer()
|
||||||
Picker("", selection: Binding(
|
Picker("", selection: Binding(
|
||||||
get: { accessMode },
|
get: { accessMode },
|
||||||
|
|
@ -402,13 +405,53 @@ struct DashboardSettingsView: View {
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
Text(accessMode.description)
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
.font(.caption)
|
Text(accessMode.description)
|
||||||
.foregroundStyle(.secondary)
|
.font(.caption)
|
||||||
}
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
Divider()
|
// Show IP address when network access is enabled
|
||||||
.padding(.vertical, 4)
|
if accessMode == .network {
|
||||||
|
if let ipAddress = localIPAddress {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text("Access from other devices at:")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
let urlString = "http://\(ipAddress):\(serverPort)"
|
||||||
|
if let url = URL(string: urlString) {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("http://\(ipAddress):\(serverPort)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
.underline()
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.cursor(.pointingHand)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
let urlString = "http://\(ipAddress):\(serverPort)"
|
||||||
|
NSPasteboard.general.clearContents()
|
||||||
|
NSPasteboard.general.setString(urlString, forType: .string)
|
||||||
|
}) {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Copy URL")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Unable to determine local IP address")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Port Configuration
|
// Port Configuration
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
|
@ -606,6 +649,13 @@ struct DashboardSettingsView: View {
|
||||||
if ngrokTokenPresent && !isTokenRevealed {
|
if ngrokTokenPresent && !isTokenRevealed {
|
||||||
maskedToken = String(repeating: "•", count: 12)
|
maskedToken = String(repeating: "•", count: 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get local IP address
|
||||||
|
updateLocalIPAddress()
|
||||||
|
}
|
||||||
|
.onChange(of: accessMode) { _, _ in
|
||||||
|
// Update IP address when access mode changes
|
||||||
|
updateLocalIPAddress()
|
||||||
}
|
}
|
||||||
.alert("ngrok Auth Token Required", isPresented: $showingAuthTokenAlert) {
|
.alert("ngrok Auth Token Required", isPresented: $showingAuthTokenAlert) {
|
||||||
Button("OK") {}
|
Button("OK") {}
|
||||||
|
|
@ -762,6 +812,16 @@ struct DashboardSettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateLocalIPAddress() {
|
||||||
|
Task {
|
||||||
|
if accessMode == .network {
|
||||||
|
localIPAddress = NetworkUtility.getLocalIPAddress()
|
||||||
|
} else {
|
||||||
|
localIPAddress = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advanced settings tab for power user options
|
/// Advanced settings tab for power user options
|
||||||
|
|
@ -774,6 +834,7 @@ struct AdvancedSettingsView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
// Integration section
|
||||||
Section {
|
Section {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
@ -788,14 +849,26 @@ struct AdvancedSettingsView: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Integration")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced section
|
||||||
|
Section {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Toggle("Clean up old sessions on startup", isOn: $cleanupOnStartup)
|
Toggle("Clean up old sessions on startup", isOn: $cleanupOnStartup)
|
||||||
Text("Automatically remove terminated sessions when the app starts.")
|
Text("Automatically remove terminated sessions when the app starts.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Advanced")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug section
|
||||||
|
Section {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Toggle("Debug mode", isOn: $debugMode)
|
Toggle("Debug mode", isOn: $debugMode)
|
||||||
Text("Enable additional logging and debugging features.")
|
Text("Enable additional logging and debugging features.")
|
||||||
|
|
@ -803,7 +876,7 @@ struct AdvancedSettingsView: View {
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Advanced")
|
Text("Debug")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -920,7 +993,12 @@ struct DebugSettingsView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
Picker("", selection: Binding(
|
Picker("", selection: Binding(
|
||||||
get: { ServerMode(rawValue: serverModeString) ?? .hummingbird },
|
get: { ServerMode(rawValue: serverModeString) ?? .hummingbird },
|
||||||
set: { serverModeString = $0.rawValue }
|
set: { newMode in
|
||||||
|
serverModeString = newMode.rawValue
|
||||||
|
Task {
|
||||||
|
await serverManager.switchMode(to: newMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
)) {
|
)) {
|
||||||
ForEach(ServerMode.allCases, id: \.self) { mode in
|
ForEach(ServerMode.allCases, id: \.self) { mode in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
@ -984,7 +1062,7 @@ struct DebugSettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
LabeledContent("Mode") {
|
LabeledContent("Mode") {
|
||||||
Text(serverManager.currentServer?.serverType.displayName ?? "None")
|
Text(getCurrentServerMode())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1171,6 +1249,7 @@ struct DebugSettingsView: View {
|
||||||
// Clear health status when switching modes
|
// Clear health status when switching modes
|
||||||
isServerHealthy = false
|
isServerHealthy = false
|
||||||
}
|
}
|
||||||
|
// Server changes are automatically observed through serverManager
|
||||||
.alert("Purge All User Defaults?", isPresented: $showPurgeConfirmation) {
|
.alert("Purge All User Defaults?", isPresented: $showPurgeConfirmation) {
|
||||||
Button("Cancel", role: .cancel) {}
|
Button("Cancel", role: .cancel) {}
|
||||||
Button("Purge", role: .destructive) {
|
Button("Purge", role: .destructive) {
|
||||||
|
|
@ -1329,6 +1408,21 @@ struct DebugSettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getCurrentServerMode() -> String {
|
||||||
|
// If server is switching, show transitioning state
|
||||||
|
if serverManager.isSwitching {
|
||||||
|
return "Switching..."
|
||||||
|
}
|
||||||
|
|
||||||
|
// If server is running and we have a current server, use its type
|
||||||
|
if isServerRunning, let serverType = serverManager.currentServer?.serverType {
|
||||||
|
return serverType.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, show the configured mode from settings
|
||||||
|
return ServerMode(rawValue: serverModeString)?.displayName ?? "None"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// API Endpoint data
|
/// API Endpoint data
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue