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)
.controlSize(.small)
}
} else if tailscaleService.isRunning {
// 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()
} else if !tailscaleService.isRunning {
// Show Tailscale preferences even when not running
VStack(alignment: .leading, spacing: 12) {
// Tailscale Serve toggle - always available when installed
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()
}
}
}
Spacer()
Spacer()
if tailscaleServeEnabled {
// Show status indicator - fixed height to prevent jumping
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 {
// Show status when enabled but not running
if tailscaleServeEnabled {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.help("Error: \(error)")
Text("Error")
Text("Tailscale not running")
.font(.caption)
.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
if let hostname = tailscaleService.tailscaleHostname {
InlineClickableURLView(
label: "Access VibeTunnel at:",
url: TailscaleURLHelper.constructURL(
hostname: hostname,
port: serverPort,
isTailscaleServeEnabled: tailscaleServeEnabled
)?.absoluteString ?? ""
)
Spacer()
// 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))
if tailscaleServeEnabled {
// Show status indicator - fixed height to prevent jumping
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")
.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(
"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)
.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

@ -87,6 +87,11 @@ struct AccessModeView: View {
@AppStorage(AppConstants.UserDefaultsKeys.tailscaleServeEnabled)
private var tailscaleServeEnabled = false
@Environment(TailscaleService.self)
private var tailscaleService
@Environment(TailscaleServeStatusService.self)
private var tailscaleServeStatus
var body: some View {
VStack(alignment: .leading, spacing: 8) {
@ -95,14 +100,14 @@ struct AccessModeView: View {
.font(.callout)
Spacer()
if tailscaleServeEnabled {
// When Tailscale Serve is enabled, force localhost mode
if shouldLockToLocalhost {
// Only lock when Tailscale Serve is actually working
Text("Localhost")
.foregroundColor(.secondary)
Image(systemName: "lock.shield.fill")
.foregroundColor(.blue)
.help("Tailscale Serve requires localhost binding for security")
.help("Tailscale Serve active - locked to localhost for security")
} else {
Picker("", selection: $accessModeString) {
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) {
Image(systemName: "info.circle.fill")
.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