mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
feat: add Tailscale integration and improve networking documentation (#144)
- Add TailscaleService for status checking and integration - Add Tailscale section in dashboard settings with status display - Expand README with detailed setup guides for Tailscale and ngrok - Show Tailscale hostname and IP when connected - Add links to download/setup resources for both services 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f1c0554644
commit
9cea558da8
6 changed files with 580 additions and 13 deletions
44
README.md
44
README.md
|
|
@ -77,13 +77,47 @@ The server runs as a standalone Bun executable with embedded Node.js modules, pr
|
|||
## Remote Access Options
|
||||
|
||||
### Option 1: Tailscale (Recommended)
|
||||
1. Install [Tailscale](https://tailscale.com) on your Mac and remote device
|
||||
2. Access VibeTunnel at `http://[your-mac-name]:4020`
|
||||
|
||||
[Tailscale](https://tailscale.com) creates a secure peer-to-peer VPN network between your devices. It's the most secure option as traffic stays within your private network without exposing VibeTunnel to the public internet.
|
||||
|
||||
**How it works**: Tailscale creates an encrypted WireGuard tunnel between your devices, allowing them to communicate as if they were on the same local network, regardless of their physical location.
|
||||
|
||||
**Setup Guide**:
|
||||
1. Install Tailscale on your Mac: [Download from Mac App Store](https://apps.apple.com/us/app/tailscale/id1475387142) or [Direct Download](https://tailscale.com/download/macos)
|
||||
2. Install Tailscale on your remote device:
|
||||
- **iOS**: [Download from App Store](https://apps.apple.com/us/app/tailscale/id1470499037)
|
||||
- **Android**: [Download from Google Play](https://play.google.com/store/apps/details?id=com.tailscale.ipn)
|
||||
- **Other platforms**: [All Downloads](https://tailscale.com/download)
|
||||
3. Sign in to both devices with the same account
|
||||
4. Find your Mac's Tailscale hostname in the Tailscale menu bar app (e.g., `my-mac.tailnet-name.ts.net`)
|
||||
5. Access VibeTunnel at `http://[your-tailscale-hostname]:4020`
|
||||
|
||||
**Benefits**:
|
||||
- End-to-end encrypted traffic
|
||||
- No public internet exposure
|
||||
- Works behind NAT and firewalls
|
||||
- Zero configuration after initial setup
|
||||
|
||||
### Option 2: ngrok
|
||||
1. Add your ngrok auth token in VibeTunnel settings
|
||||
2. Enable ngrok tunneling
|
||||
3. Share the generated URL
|
||||
|
||||
[ngrok](https://ngrok.com) creates secure tunnels to your localhost, making VibeTunnel accessible via a public URL. Perfect for quick sharing or temporary access.
|
||||
|
||||
**How it works**: ngrok establishes a secure tunnel from a public endpoint to your local VibeTunnel server, handling SSL/TLS encryption and providing a unique URL for access.
|
||||
|
||||
**Setup Guide**:
|
||||
1. Create a free ngrok account: [Sign up for ngrok](https://dashboard.ngrok.com/signup)
|
||||
2. Copy your auth token from the [ngrok dashboard](https://dashboard.ngrok.com/get-started/your-authtoken)
|
||||
3. Add the token in VibeTunnel settings (Settings → Remote Access → ngrok)
|
||||
4. Enable ngrok tunneling in VibeTunnel
|
||||
5. Share the generated `https://[random].ngrok-free.app` URL
|
||||
|
||||
**Benefits**:
|
||||
- Public HTTPS URL with SSL certificate
|
||||
- No firewall configuration needed
|
||||
- Built-in request inspection and replay
|
||||
- Custom domains available (paid plans)
|
||||
|
||||
**Note**: Free ngrok URLs change each time you restart the tunnel. Consider a paid plan for persistent URLs.
|
||||
|
||||
### Option 3: Local Network
|
||||
1. Set a dashboard password in settings
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ class ServerManager {
|
|||
|
||||
var bindAddress: String {
|
||||
get {
|
||||
let mode = DashboardAccessMode(rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? ""
|
||||
let mode = DashboardAccessMode(
|
||||
rawValue: UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? ""
|
||||
) ??
|
||||
.localhost
|
||||
return mode.bindAddress
|
||||
|
|
@ -466,7 +467,8 @@ class ServerManager {
|
|||
let delay = baseDelay * pow(2.0, Double(consecutiveCrashes - 1))
|
||||
|
||||
logger
|
||||
.info("Will restart server after \(delay) seconds (attempt \(self.consecutiveCrashes) of \(maxRetries))"
|
||||
.info(
|
||||
"Will restart server after \(delay) seconds (attempt \(self.consecutiveCrashes) of \(maxRetries))"
|
||||
)
|
||||
|
||||
// Wait with exponential backoff
|
||||
|
|
|
|||
268
mac/VibeTunnel/Core/Services/TailscaleService.swift
Normal file
268
mac/VibeTunnel/Core/Services/TailscaleService.swift
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import AppKit
|
||||
import Foundation
|
||||
import Observation
|
||||
import os
|
||||
|
||||
/// Manages Tailscale integration and status checking.
|
||||
///
|
||||
/// `TailscaleService` provides functionality to check if Tailscale is installed
|
||||
/// and running on the system, and retrieves the device's Tailscale hostname
|
||||
/// for network access. Unlike ngrok, Tailscale doesn't require auth tokens
|
||||
/// as it uses system-level authentication.
|
||||
@Observable
|
||||
@MainActor
|
||||
final class TailscaleService {
|
||||
static let shared = TailscaleService()
|
||||
|
||||
/// Logger instance for debugging
|
||||
private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "TailscaleService")
|
||||
|
||||
/// Indicates if Tailscale app is installed on the system
|
||||
private(set) var isInstalled = false
|
||||
|
||||
/// Indicates if Tailscale CLI is available
|
||||
private(set) var isCLIAvailable = false
|
||||
|
||||
/// Indicates if Tailscale is currently running
|
||||
private(set) var isRunning = false
|
||||
|
||||
/// The Tailscale hostname for this device (e.g., "my-mac.tailnet-name.ts.net")
|
||||
private(set) var tailscaleHostname: String?
|
||||
|
||||
/// The Tailscale IP address for this device
|
||||
private(set) var tailscaleIP: String?
|
||||
|
||||
/// Error message if status check fails
|
||||
private(set) var statusError: String?
|
||||
|
||||
/// Path to the tailscale executable
|
||||
private var tailscalePath: String?
|
||||
|
||||
private init() {
|
||||
Task {
|
||||
await checkTailscaleStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if Tailscale app is installed
|
||||
func checkAppInstallation() -> Bool {
|
||||
let isAppInstalled = FileManager.default.fileExists(atPath: "/Applications/Tailscale.app")
|
||||
logger.info("Tailscale app installed: \(isAppInstalled)")
|
||||
return isAppInstalled
|
||||
}
|
||||
|
||||
/// Checks if Tailscale CLI is available
|
||||
func checkCLIAvailability() async -> Bool {
|
||||
let checkPaths = [
|
||||
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
|
||||
"/usr/local/bin/tailscale",
|
||||
"/opt/homebrew/bin/tailscale"
|
||||
]
|
||||
|
||||
for path in checkPaths {
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
logger.info("Tailscale CLI found at: \(path)")
|
||||
tailscalePath = path
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if we can run the tailscale command using which
|
||||
do {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/which")
|
||||
process.arguments = ["tailscale"]
|
||||
|
||||
// Set up PATH to include common installation directories
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
let additionalPaths = [
|
||||
"/usr/local/bin",
|
||||
"/opt/homebrew/bin",
|
||||
"/Applications/Tailscale.app/Contents/MacOS"
|
||||
]
|
||||
if let currentPath = environment["PATH"] {
|
||||
environment["PATH"] = "\(currentPath):\(additionalPaths.joined(separator: ":"))"
|
||||
} else {
|
||||
environment["PATH"] = additionalPaths.joined(separator: ":")
|
||||
}
|
||||
process.environment = environment
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
if process.terminationStatus == 0 {
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!output.isEmpty
|
||||
{
|
||||
logger.info("Tailscale CLI found at: \(output)")
|
||||
tailscalePath = output
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.debug("Failed to check for tailscale command: \(error)")
|
||||
}
|
||||
|
||||
logger.info("Tailscale CLI not found")
|
||||
tailscalePath = nil
|
||||
return false
|
||||
}
|
||||
|
||||
/// Checks the current Tailscale status and updates properties
|
||||
func checkTailscaleStatus() async {
|
||||
// First check if app is installed
|
||||
isInstalled = checkAppInstallation()
|
||||
|
||||
guard isInstalled else {
|
||||
isCLIAvailable = false
|
||||
isRunning = false
|
||||
tailscaleHostname = nil
|
||||
tailscaleIP = nil
|
||||
statusError = "Tailscale is not installed"
|
||||
return
|
||||
}
|
||||
|
||||
// Then check if CLI is available
|
||||
isCLIAvailable = await checkCLIAvailability()
|
||||
|
||||
guard isCLIAvailable else {
|
||||
isRunning = false
|
||||
tailscaleHostname = nil
|
||||
tailscaleIP = nil
|
||||
statusError = nil // No error, just CLI not available
|
||||
return
|
||||
}
|
||||
|
||||
// If CLI is available, check status
|
||||
do {
|
||||
let process = Process()
|
||||
|
||||
// Use the discovered tailscale path
|
||||
if let tailscalePath {
|
||||
process.executableURL = URL(fileURLWithPath: tailscalePath)
|
||||
process.arguments = ["status", "--json"]
|
||||
} else {
|
||||
// Fallback to env if path not found (shouldn't happen if isCLIAvailable is true)
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
process.arguments = ["tailscale", "status", "--json"]
|
||||
|
||||
// Set up PATH environment variable
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
let additionalPaths = [
|
||||
"/usr/local/bin",
|
||||
"/opt/homebrew/bin",
|
||||
"/Applications/Tailscale.app/Contents/MacOS"
|
||||
]
|
||||
if let currentPath = environment["PATH"] {
|
||||
environment["PATH"] = "\(currentPath):\(additionalPaths.joined(separator: ":"))"
|
||||
} else {
|
||||
environment["PATH"] = additionalPaths.joined(separator: ":")
|
||||
}
|
||||
process.environment = environment
|
||||
}
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
|
||||
if process.terminationStatus == 0 {
|
||||
// Parse JSON output
|
||||
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
// Check if we're logged in and connected
|
||||
if let self_ = json["Self"] as? [String: Any],
|
||||
let dnsName = self_["DNSName"] as? String
|
||||
{
|
||||
// Check online status - it might be missing or false
|
||||
let online = self_["Online"] as? Bool ?? false
|
||||
isRunning = online
|
||||
|
||||
// Use the DNSName which is already properly formatted for DNS
|
||||
// Remove trailing dot if present
|
||||
tailscaleHostname = dnsName.hasSuffix(".") ? String(dnsName.dropLast()) : dnsName
|
||||
|
||||
// Get Tailscale IP
|
||||
if let tailscaleIPs = self_["TailscaleIPs"] as? [String],
|
||||
let firstIP = tailscaleIPs.first
|
||||
{
|
||||
tailscaleIP = firstIP
|
||||
}
|
||||
|
||||
statusError = nil
|
||||
logger
|
||||
.info(
|
||||
"Tailscale status: running=\(online), hostname=\(self.tailscaleHostname ?? "nil"), IP=\(self.tailscaleIP ?? "nil")"
|
||||
)
|
||||
} else {
|
||||
isRunning = false
|
||||
tailscaleHostname = nil
|
||||
tailscaleIP = nil
|
||||
statusError = "Tailscale is not logged in"
|
||||
logger.warning("Tailscale status check failed - missing required fields in JSON")
|
||||
logger.debug("JSON keys: \(json.keys.sorted())")
|
||||
}
|
||||
} else {
|
||||
isRunning = false
|
||||
statusError = "Failed to parse Tailscale status"
|
||||
}
|
||||
} else {
|
||||
// Tailscale CLI returned error
|
||||
let errorOutput = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||
isRunning = false
|
||||
tailscaleHostname = nil
|
||||
tailscaleIP = nil
|
||||
|
||||
if errorOutput.contains("not logged in") {
|
||||
statusError = "Tailscale is not logged in"
|
||||
} else if errorOutput.contains("stopped") {
|
||||
statusError = "Tailscale is stopped"
|
||||
} else {
|
||||
statusError = "Tailscale error: \(errorOutput.trimmingCharacters(in: .whitespacesAndNewlines))"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to check Tailscale status: \(error)")
|
||||
isRunning = false
|
||||
tailscaleHostname = nil
|
||||
tailscaleIP = nil
|
||||
statusError = "Failed to check status: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the Tailscale app
|
||||
func openTailscaleApp() {
|
||||
if let url = URL(string: "file:///Applications/Tailscale.app") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the Mac App Store page for Tailscale
|
||||
func openAppStore() {
|
||||
if let url = URL(string: "https://apps.apple.com/us/app/tailscale/id1475387142") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the Tailscale download page
|
||||
func openDownloadPage() {
|
||||
if let url = URL(string: "https://tailscale.com/download/macos") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the Tailscale setup guide
|
||||
func openSetupGuide() {
|
||||
if let url = URL(string: "https://tailscale.com/kb/1017/install/") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ struct DashboardSettingsView: View {
|
|||
private var serverManager
|
||||
@Environment(NgrokService.self)
|
||||
private var ngrokService
|
||||
@Environment(TailscaleService.self)
|
||||
private var tailscaleService
|
||||
|
||||
@State private var ngrokAuthToken = ""
|
||||
@State private var ngrokStatus: NgrokTunnelStatus?
|
||||
|
|
@ -65,6 +67,12 @@ struct DashboardSettingsView: View {
|
|||
serverManager: serverManager
|
||||
)
|
||||
|
||||
TailscaleIntegrationSection(
|
||||
tailscaleService: tailscaleService,
|
||||
serverPort: serverPort,
|
||||
accessMode: accessMode
|
||||
)
|
||||
|
||||
NgrokIntegrationSection(
|
||||
ngrokEnabled: $ngrokEnabled,
|
||||
ngrokAuthToken: $ngrokAuthToken,
|
||||
|
|
@ -647,10 +655,12 @@ private struct NgrokIntegrationSection: View {
|
|||
Text("ngrok Integration")
|
||||
.font(.headline)
|
||||
} footer: {
|
||||
Text("ngrok creates secure tunnels to your dashboard from anywhere.")
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(
|
||||
"ngrok creates secure public tunnels to access your terminal sessions from any device (including phones and tablets) via the internet."
|
||||
)
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -783,6 +793,253 @@ private struct ErrorView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Tailscale Integration Section
|
||||
|
||||
private struct TailscaleIntegrationSection: View {
|
||||
let tailscaleService: TailscaleService
|
||||
let serverPort: String
|
||||
let accessMode: DashboardAccessMode
|
||||
|
||||
@State private var statusCheckTimer: Timer?
|
||||
|
||||
private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "TailscaleIntegrationSection")
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if tailscaleService.isInstalled {
|
||||
// Tailscale app is installed
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("Tailscale is installed")
|
||||
.font(.callout)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
if tailscaleService.isCLIAvailable {
|
||||
// CLI is available, show status
|
||||
if tailscaleService.isRunning || tailscaleService.tailscaleHostname != nil {
|
||||
// Show Tailscale hostname and connection info (even if offline, as user might still
|
||||
// connect)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let hostname = tailscaleService.tailscaleHostname {
|
||||
HStack {
|
||||
Text("Tailscale hostname:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(hostname)
|
||||
.font(.caption)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
if let tailscaleIP = tailscaleService.tailscaleIP {
|
||||
HStack {
|
||||
Text("Tailscale IP:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(tailscaleIP)
|
||||
.font(.caption)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Access URL
|
||||
if let hostname = tailscaleService.tailscaleHostname {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if accessMode == .localhost {
|
||||
// Show warning if in localhost-only mode
|
||||
HStack(spacing: 4) {
|
||||
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)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
} else {
|
||||
// Show the access URL
|
||||
HStack(spacing: 5) {
|
||||
Text("Access VibeTunnel at:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
let urlString = "http://\(hostname):\(serverPort)"
|
||||
if let url = URL(string: urlString) {
|
||||
Link(urlString, destination: url)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
if !tailscaleService.isRunning {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 10))
|
||||
Text("Tailscale reports as offline but may still be accessible")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// CLI available but Tailscale not running/logged in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let error = tailscaleService.statusError {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.foregroundColor(.orange)
|
||||
Text(error)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button("Open Tailscale") {
|
||||
tailscaleService.openTailscaleApp()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
|
||||
if let url = URL(string: "https://tailscale.com/kb/1017/install/") {
|
||||
Link("Setup Guide", destination: url)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// App installed but CLI not available
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.blue)
|
||||
Text("Tailscale CLI not available")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text(
|
||||
"To see your Tailscale status here, install the Tailscale CLI. You can still use Tailscale - just open the app and connect to your tailnet."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button("Open Tailscale") {
|
||||
tailscaleService.openTailscaleApp()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
|
||||
if let url = URL(string: "https://tailscale.com/kb/1090/install-tailscale-cli/") {
|
||||
Link("Install CLI", destination: url)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Tailscale is not installed
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.blue)
|
||||
Text("Tailscale is not installed")
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Tailscale creates a secure peer-to-peer VPN for accessing VibeTunnel from any device - your phone, tablet, or another computer."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button(action: {
|
||||
tailscaleService.openAppStore()
|
||||
}) {
|
||||
Text("App Store")
|
||||
}
|
||||
.buttonStyle(.link)
|
||||
.controlSize(.small)
|
||||
|
||||
Button(action: {
|
||||
tailscaleService.openDownloadPage()
|
||||
}) {
|
||||
Text("Direct Download")
|
||||
}
|
||||
.buttonStyle(.link)
|
||||
.controlSize(.small)
|
||||
|
||||
Button(action: {
|
||||
tailscaleService.openSetupGuide()
|
||||
}) {
|
||||
Text("Setup Guide")
|
||||
}
|
||||
.buttonStyle(.link)
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
Button("Check Again") {
|
||||
Task {
|
||||
await tailscaleService.checkTailscaleStatus()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.link)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Tailscale Integration")
|
||||
.font(.headline)
|
||||
} footer: {
|
||||
Text(
|
||||
"Recommended: Tailscale provides secure, private access to your terminal sessions from any device (including phones and tablets) without exposing VibeTunnel to the public internet."
|
||||
)
|
||||
.font(.caption)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.task {
|
||||
// Check status when view appears
|
||||
logger.info("TailscaleIntegrationSection: Starting initial status check")
|
||||
await tailscaleService.checkTailscaleStatus()
|
||||
logger
|
||||
.info(
|
||||
"TailscaleIntegrationSection: Status check complete - isInstalled: \(tailscaleService.isInstalled), isCLIAvailable: \(tailscaleService.isCLIAvailable), isRunning: \(tailscaleService.isRunning), hostname: \(tailscaleService.tailscaleHostname ?? "nil")"
|
||||
)
|
||||
|
||||
// Set up timer for automatic updates every 5 seconds
|
||||
statusCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
|
||||
Task {
|
||||
logger.debug("TailscaleIntegrationSection: Running periodic status check")
|
||||
await tailscaleService.checkTailscaleStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
// Clean up timer when view disappears
|
||||
statusCheckTimer?.invalidate()
|
||||
statusCheckTimer = nil
|
||||
logger.info("TailscaleIntegrationSection: Stopped status check timer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Dashboard Settings") {
|
||||
|
|
|
|||
|
|
@ -69,8 +69,9 @@ enum SettingsOpener {
|
|||
|
||||
// Check by title
|
||||
if window.isVisible && window.styleMask.contains(.titled) &&
|
||||
(window.title.localizedCaseInsensitiveContains("settings") ||
|
||||
window.title.localizedCaseInsensitiveContains("preferences")
|
||||
(
|
||||
window.title.localizedCaseInsensitiveContains("settings") ||
|
||||
window.title.localizedCaseInsensitiveContains("preferences")
|
||||
)
|
||||
{
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ struct VibeTunnelApp: App {
|
|||
@State var sessionMonitor = SessionMonitor.shared
|
||||
@State var serverManager = ServerManager.shared
|
||||
@State var ngrokService = NgrokService.shared
|
||||
@State var tailscaleService = TailscaleService.shared
|
||||
@State var permissionManager = SystemPermissionManager.shared
|
||||
@State var terminalLauncher = TerminalLauncher.shared
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ struct VibeTunnelApp: App {
|
|||
.environment(sessionMonitor)
|
||||
.environment(serverManager)
|
||||
.environment(ngrokService)
|
||||
.environment(tailscaleService)
|
||||
.environment(permissionManager)
|
||||
.environment(terminalLauncher)
|
||||
}
|
||||
|
|
@ -52,6 +54,7 @@ struct VibeTunnelApp: App {
|
|||
.environment(sessionMonitor)
|
||||
.environment(serverManager)
|
||||
.environment(ngrokService)
|
||||
.environment(tailscaleService)
|
||||
.environment(permissionManager)
|
||||
.environment(terminalLauncher)
|
||||
} else {
|
||||
|
|
@ -66,6 +69,7 @@ struct VibeTunnelApp: App {
|
|||
.environment(sessionMonitor)
|
||||
.environment(serverManager)
|
||||
.environment(ngrokService)
|
||||
.environment(tailscaleService)
|
||||
.environment(permissionManager)
|
||||
.environment(terminalLauncher)
|
||||
}
|
||||
|
|
@ -90,6 +94,7 @@ struct VibeTunnelApp: App {
|
|||
.environment(sessionMonitor)
|
||||
.environment(serverManager)
|
||||
.environment(ngrokService)
|
||||
.environment(tailscaleService)
|
||||
.environment(permissionManager)
|
||||
.environment(terminalLauncher)
|
||||
} label: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue