mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Ensure tailscale can be disabled when not running
This commit is contained in:
parent
8814f2623f
commit
459cf52ef6
2 changed files with 160 additions and 82 deletions
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue