Ensure tailscale can be disabled when not running

This commit is contained in:
Peter Steinberger 2025-08-01 00:45:22 +02:00
parent 8814f2623f
commit 459cf52ef6
2 changed files with 160 additions and 82 deletions

View file

@ -311,102 +311,155 @@ private struct TailscaleIntegrationSection: View {
.buttonStyle(.link) .buttonStyle(.link)
.controlSize(.small) .controlSize(.small)
} }
} else if tailscaleService.isRunning { } else if !tailscaleService.isRunning {
// Tailscale Serve toggle // Show Tailscale preferences even when not running
HStack { VStack(alignment: .leading, spacing: 12) {
Toggle("Enable Tailscale Serve Integration", isOn: $tailscaleServeEnabled) // Tailscale Serve toggle - always available when installed
.onChange(of: tailscaleServeEnabled) { _, newValue in HStack {
logger.info("Tailscale Serve integration \(newValue ? "enabled" : "disabled")") Toggle("Enable Tailscale Serve Integration", isOn: $tailscaleServeEnabled)
// Restart server to apply the new setting .onChange(of: tailscaleServeEnabled) { _, newValue in
Task { logger.info("Tailscale Serve integration \(newValue ? "enabled" : "disabled")")
await serverManager.restart() // Restart server to apply the new setting
Task {
await serverManager.restart()
}
} }
}
Spacer() Spacer()
if tailscaleServeEnabled { // Show status when enabled but not running
// Show status indicator - fixed height to prevent jumping if tailscaleServeEnabled {
HStack(spacing: 4) { HStack(spacing: 4) {
if tailscaleServeStatus.isLoading {
ProgressView()
.scaleEffect(0.7)
} else if tailscaleServeStatus.isRunning {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Running")
.font(.caption)
.foregroundColor(.secondary)
} else if let error = tailscaleServeStatus.lastError {
Image(systemName: "exclamationmark.triangle.fill") Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange) .foregroundColor(.orange)
.help("Error: \(error)") Text("Tailscale not running")
Text("Error")
.font(.caption) .font(.caption)
.foregroundColor(.orange) .foregroundColor(.orange)
} else {
Image(systemName: "circle")
.foregroundColor(.gray)
Text("Starting...")
.font(.caption)
.foregroundColor(.secondary)
} }
.frame(height: 16)
} }
.frame(height: 16) // Fixed height prevents UI jumping }
// Show action button to start Tailscale
if tailscaleService.isInstalled && !tailscaleService.isRunning {
Button(action: {
tailscaleService.openTailscaleApp()
}, label: {
HStack(spacing: 4) {
Image(systemName: "play.circle")
Text("Start Tailscale")
}
})
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
// Show help text about what will happen when enabled
if tailscaleServeEnabled {
Text("Tailscale Serve will activate automatically when Tailscale is running.")
.font(.caption)
.foregroundColor(.secondary)
} }
} }
} else {
// Tailscale is running - show full interface
VStack(alignment: .leading, spacing: 12) {
// Tailscale Serve toggle
HStack {
Toggle("Enable Tailscale Serve Integration", isOn: $tailscaleServeEnabled)
.onChange(of: tailscaleServeEnabled) { _, newValue in
logger.info("Tailscale Serve integration \(newValue ? "enabled" : "disabled")")
// Restart server to apply the new setting
Task {
await serverManager.restart()
}
}
// Show dashboard URL when running Spacer()
if let hostname = tailscaleService.tailscaleHostname {
InlineClickableURLView(
label: "Access VibeTunnel at:",
url: TailscaleURLHelper.constructURL(
hostname: hostname,
port: serverPort,
isTailscaleServeEnabled: tailscaleServeEnabled
)?.absoluteString ?? ""
)
// Show warning if in localhost-only mode if tailscaleServeEnabled {
if accessMode == .localhost && !tailscaleServeEnabled { // Show status indicator - fixed height to prevent jumping
HStack(spacing: 6) { HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle.fill") if tailscaleServeStatus.isLoading {
.foregroundColor(.orange) ProgressView()
.font(.system(size: 12)) .scaleEffect(0.7)
} else if tailscaleServeStatus.isRunning {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Running")
.font(.caption)
.foregroundColor(.secondary)
} else if let error = tailscaleServeStatus.lastError {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.help("Error: \(error)")
Text("Error")
.font(.caption)
.foregroundColor(.orange)
} else {
Image(systemName: "circle")
.foregroundColor(.gray)
Text("Starting...")
.font(.caption)
.foregroundColor(.secondary)
}
}
.frame(height: 16) // Fixed height prevents UI jumping
}
}
// Show dashboard URL when running
if let hostname = tailscaleService.tailscaleHostname {
InlineClickableURLView(
label: "Access VibeTunnel at:",
url: TailscaleURLHelper.constructURL(
hostname: hostname,
port: serverPort,
isTailscaleServeEnabled: tailscaleServeEnabled
)?.absoluteString ?? ""
)
// Show warning if in localhost-only mode
if accessMode == .localhost && !tailscaleServeEnabled {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.system(size: 12))
Text(
"Server is in localhost-only mode. Change to 'Network' mode above to access via Tailscale."
)
.font(.caption)
.foregroundStyle(.secondary)
}
}
// Show error details if any
if tailscaleServeEnabled, let error = tailscaleServeStatus.lastError {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.system(size: 12))
Text("Error: \(error)")
.font(.caption)
.foregroundColor(.orange)
.lineLimit(2)
}
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color.orange.opacity(0.1))
.cornerRadius(4)
}
// Help text about Tailscale Serve
if tailscaleServeEnabled && tailscaleServeStatus.isRunning {
Text( Text(
"Server is in localhost-only mode. Change to 'Network' mode above to access via Tailscale." "Tailscale Serve provides secure access with automatic authentication using Tailscale identity headers."
) )
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundColor(.secondary)
.padding(.top, 4)
} }
} }
// Show error details if any
if tailscaleServeEnabled, let error = tailscaleServeStatus.lastError {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.system(size: 12))
Text("Error: \(error)")
.font(.caption)
.foregroundColor(.orange)
.lineLimit(2)
}
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color.orange.opacity(0.1))
.cornerRadius(4)
}
// Help text about Tailscale Serve
if tailscaleServeEnabled && tailscaleServeStatus.isRunning {
Text(
"Tailscale Serve provides secure access with automatic authentication using Tailscale identity headers."
)
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 4)
}
} }
} }
} }

View file

@ -88,6 +88,11 @@ struct AccessModeView: View {
@AppStorage(AppConstants.UserDefaultsKeys.tailscaleServeEnabled) @AppStorage(AppConstants.UserDefaultsKeys.tailscaleServeEnabled)
private var tailscaleServeEnabled = false private var tailscaleServeEnabled = false
@Environment(TailscaleService.self)
private var tailscaleService
@Environment(TailscaleServeStatusService.self)
private var tailscaleServeStatus
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
@ -95,14 +100,14 @@ struct AccessModeView: View {
.font(.callout) .font(.callout)
Spacer() Spacer()
if tailscaleServeEnabled { if shouldLockToLocalhost {
// When Tailscale Serve is enabled, force localhost mode // Only lock when Tailscale Serve is actually working
Text("Localhost") Text("Localhost")
.foregroundColor(.secondary) .foregroundColor(.secondary)
Image(systemName: "lock.shield.fill") Image(systemName: "lock.shield.fill")
.foregroundColor(.blue) .foregroundColor(.blue)
.help("Tailscale Serve requires localhost binding for security") .help("Tailscale Serve active - locked to localhost for security")
} else { } else {
Picker("", selection: $accessModeString) { Picker("", selection: $accessModeString) {
ForEach(DashboardAccessMode.allCases, id: \.rawValue) { mode in ForEach(DashboardAccessMode.allCases, id: \.rawValue) { mode in
@ -117,7 +122,20 @@ struct AccessModeView: View {
} }
} }
if tailscaleServeEnabled && accessMode == .network { // Show warning when Tailscale Serve is enabled but not working
if tailscaleServeEnabled && !shouldLockToLocalhost {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.caption)
Text("Tailscale Serve enabled but not active - using selected access mode")
.font(.caption)
.foregroundColor(.secondary)
}
}
// Show info when Tailscale Serve is active and locked
if shouldLockToLocalhost && accessMode == .network {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "info.circle.fill") Image(systemName: "info.circle.fill")
.foregroundColor(.blue) .foregroundColor(.blue)
@ -129,6 +147,13 @@ struct AccessModeView: View {
} }
} }
} }
/// Only lock to localhost when Tailscale Serve is enabled AND actually working
private var shouldLockToLocalhost: Bool {
tailscaleServeEnabled &&
tailscaleService.isRunning &&
tailscaleServeStatus.isRunning
}
} }
// MARK: - Port Configuration View // MARK: - Port Configuration View