Show computer IP when network is enabled

This commit is contained in:
Peter Steinberger 2025-06-17 00:27:58 +02:00
parent 929808dbb2
commit fc31cac55f
2 changed files with 198 additions and 12 deletions

View 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
}
}

View file

@ -290,6 +290,7 @@ struct DashboardSettingsView: View {
@State private var serverErrorMessage = ""
@State private var isTokenRevealed = false
@State private var maskedToken = ""
@State private var localIPAddress: String?
private let dashboardKeychain = DashboardKeychain.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."
)
.font(.caption)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
}
Section {
// Access Mode
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Allow accessing dashboard:")
Text("Allow accessing the dashboard from:")
Spacer()
Picker("", selection: Binding(
get: { accessMode },
@ -402,14 +405,54 @@ struct DashboardSettingsView: View {
.pickerStyle(.menu)
.labelsHidden()
}
Text(accessMode.description)
.font(.caption)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 4) {
Text(accessMode.description)
.font(.caption)
.foregroundStyle(.secondary)
// Show IP address when network access is enabled
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)
}
}
}
}
Divider()
.padding(.vertical, 4)
// Port Configuration
VStack(alignment: .leading, spacing: 4) {
HStack {
@ -606,6 +649,13 @@ struct DashboardSettingsView: View {
if ngrokTokenPresent && !isTokenRevealed {
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) {
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
@ -774,6 +834,7 @@ struct AdvancedSettingsView: View {
var body: some View {
NavigationStack {
Form {
// Integration section
Section {
VStack(alignment: .leading, spacing: 8) {
HStack {
@ -788,14 +849,26 @@ struct AdvancedSettingsView: View {
.font(.caption)
.foregroundStyle(.secondary)
}
} header: {
Text("Integration")
.font(.headline)
}
// Advanced section
Section {
VStack(alignment: .leading, spacing: 4) {
Toggle("Clean up old sessions on startup", isOn: $cleanupOnStartup)
Text("Automatically remove terminated sessions when the app starts.")
.font(.caption)
.foregroundStyle(.secondary)
}
} header: {
Text("Advanced")
.font(.headline)
}
// Debug section
Section {
VStack(alignment: .leading, spacing: 4) {
Toggle("Debug mode", isOn: $debugMode)
Text("Enable additional logging and debugging features.")
@ -803,7 +876,7 @@ struct AdvancedSettingsView: View {
.foregroundStyle(.secondary)
}
} header: {
Text("Advanced")
Text("Debug")
.font(.headline)
}
}
@ -920,7 +993,12 @@ struct DebugSettingsView: View {
Spacer()
Picker("", selection: Binding(
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
VStack(alignment: .leading) {
@ -984,7 +1062,7 @@ struct DebugSettingsView: View {
}
LabeledContent("Mode") {
Text(serverManager.currentServer?.serverType.displayName ?? "None")
Text(getCurrentServerMode())
.foregroundStyle(.secondary)
}
}
@ -1171,6 +1249,7 @@ struct DebugSettingsView: View {
// Clear health status when switching modes
isServerHealthy = false
}
// Server changes are automatically observed through serverManager
.alert("Purge All User Defaults?", isPresented: $showPurgeConfirmation) {
Button("Cancel", role: .cancel) {}
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