mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
341 lines
12 KiB
Swift
341 lines
12 KiB
Swift
import AppKit
|
|
import os.log
|
|
import SwiftUI
|
|
|
|
// MARK: - Dev Server Validation
|
|
|
|
enum DevServerValidation: Equatable {
|
|
case notValidated
|
|
case validating
|
|
case valid
|
|
case invalid(String)
|
|
|
|
var isValid: Bool {
|
|
if case .valid = self { return true }
|
|
return false
|
|
}
|
|
|
|
var errorMessage: String? {
|
|
if case .invalid(let message) = self { return message }
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Debug settings tab for development and troubleshooting
|
|
struct DebugSettingsView: View {
|
|
@AppStorage(AppConstants.UserDefaultsKeys.debugMode)
|
|
private var debugMode = false
|
|
@AppStorage(AppConstants.UserDefaultsKeys.logLevel)
|
|
private var logLevel = "info"
|
|
@AppStorage(AppConstants.UserDefaultsKeys.useDevServer)
|
|
private var useDevServer = false
|
|
@AppStorage(AppConstants.UserDefaultsKeys.devServerPath)
|
|
private var devServerPath = ""
|
|
@Environment(ServerManager.self)
|
|
private var serverManager
|
|
@State private var showPurgeConfirmation = false
|
|
@State private var devServerValidation: DevServerValidation = .notValidated
|
|
@State private var devServerManager = DevServerManager.shared
|
|
|
|
private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "DebugSettings")
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
DevelopmentServerSection(
|
|
useDevServer: $useDevServer,
|
|
devServerPath: $devServerPath,
|
|
devServerValidation: $devServerValidation,
|
|
validateDevServer: validateDevServer,
|
|
serverManager: serverManager
|
|
)
|
|
|
|
DebugOptionsSection(
|
|
debugMode: $debugMode,
|
|
logLevel: $logLevel
|
|
)
|
|
|
|
DeveloperToolsSection(
|
|
showPurgeConfirmation: $showPurgeConfirmation,
|
|
openConsole: openConsole,
|
|
showApplicationSupport: showApplicationSupport
|
|
)
|
|
}
|
|
.formStyle(.grouped)
|
|
.scrollContentBackground(.hidden)
|
|
.navigationTitle("Debug Settings")
|
|
.alert("Purge All User Defaults?", isPresented: $showPurgeConfirmation) {
|
|
Button("Cancel", role: .cancel) {}
|
|
Button("Purge", role: .destructive) {
|
|
purgeAllUserDefaults()
|
|
}
|
|
} message: {
|
|
Text(
|
|
"This will remove all stored preferences and reset the app to its default state. The app will quit after purging."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
private func purgeAllUserDefaults() {
|
|
// Get the app's bundle identifier
|
|
if let bundleIdentifier = Bundle.main.bundleIdentifier {
|
|
// Remove all UserDefaults for this app
|
|
UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier)
|
|
UserDefaults.standard.synchronize()
|
|
|
|
// Quit the app after a short delay to ensure the purge completes
|
|
Task {
|
|
try? await Task.sleep(for: .milliseconds(500))
|
|
await MainActor.run {
|
|
NSApplication.shared.terminate(nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func openConsole() {
|
|
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/Utilities/Console.app"))
|
|
}
|
|
|
|
private func showApplicationSupport() {
|
|
if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
|
|
let appDirectory = appSupport.appendingPathComponent("VibeTunnel")
|
|
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: appDirectory.path)
|
|
}
|
|
}
|
|
|
|
private func validateDevServer(path: String) {
|
|
devServerValidation = devServerManager.validate(path: path)
|
|
}
|
|
}
|
|
|
|
// MARK: - Debug Options Section
|
|
|
|
private struct DebugOptionsSection: View {
|
|
@Binding var debugMode: Bool
|
|
@Binding var logLevel: String
|
|
@AppStorage("verboseStatusCheckLogging")
|
|
private var verboseStatusCheckLogging = false
|
|
|
|
var body: some View {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack {
|
|
Text("Log Level")
|
|
Spacer()
|
|
Picker("", selection: $logLevel) {
|
|
Text("Error").tag("error")
|
|
Text("Warning").tag("warning")
|
|
Text("Info").tag("info")
|
|
Text("Debug").tag("debug")
|
|
}
|
|
.pickerStyle(.menu)
|
|
.labelsHidden()
|
|
}
|
|
Text("Set the verbosity of application logs.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Toggle("Verbose Status Check Logging", isOn: $verboseStatusCheckLogging)
|
|
Text("Log detailed information about remote service status checks.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} header: {
|
|
Text("Debug Options")
|
|
.font(.headline)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Developer Tools Section
|
|
|
|
private struct DeveloperToolsSection: View {
|
|
@Binding var showPurgeConfirmation: Bool
|
|
let openConsole: () -> Void
|
|
let showApplicationSupport: () -> Void
|
|
|
|
var body: some View {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text("System Logs")
|
|
Spacer()
|
|
Button("Open Console") {
|
|
openConsole()
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
Text("View all application logs in Console.app.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text("Welcome Screen")
|
|
Spacer()
|
|
Button("Show Welcome") {
|
|
#if !SWIFT_PACKAGE
|
|
AppDelegate.showWelcomeScreen()
|
|
#endif
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
Text("Display the welcome screen again.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text("User Defaults")
|
|
Spacer()
|
|
Button("Purge All") {
|
|
showPurgeConfirmation = true
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.red)
|
|
}
|
|
Text("Remove all stored preferences and reset to defaults.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} header: {
|
|
Text("Developer Tools")
|
|
.font(.headline)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Development Server Section
|
|
|
|
private struct DevelopmentServerSection: View {
|
|
@Binding var useDevServer: Bool
|
|
@Binding var devServerPath: String
|
|
@Binding var devServerValidation: DevServerValidation
|
|
let validateDevServer: (String) -> Void
|
|
let serverManager: ServerManager
|
|
|
|
var body: some View {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
// Toggle for using dev server
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Toggle("Use development server", isOn: $useDevServer)
|
|
.onChange(of: useDevServer) { _, newValue in
|
|
if newValue && !devServerPath.isEmpty {
|
|
validateDevServer(devServerPath)
|
|
}
|
|
// Restart server if it's running and the setting changed
|
|
if serverManager.isRunning {
|
|
Task {
|
|
await serverManager.restart()
|
|
}
|
|
}
|
|
}
|
|
Text("Run the web server in development mode with hot reload instead of using the built-in server.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
// Path input (only shown when enabled)
|
|
if useDevServer {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(spacing: 8) {
|
|
TextField("Web project path", text: $devServerPath)
|
|
.textFieldStyle(.roundedBorder)
|
|
.onChange(of: devServerPath) { _, newPath in
|
|
validateDevServer(newPath)
|
|
}
|
|
|
|
Button(action: selectDirectory) {
|
|
Image(systemName: "folder")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("Choose directory")
|
|
}
|
|
|
|
// Validation status
|
|
if devServerValidation == .validating {
|
|
HStack(spacing: 4) {
|
|
ProgressView()
|
|
.scaleEffect(0.7)
|
|
Text("Validating...")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} else if devServerValidation.isValid {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundColor(.green)
|
|
.font(.caption)
|
|
Text("Valid project with 'pnpm run dev' script")
|
|
.font(.caption)
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if let error = devServerValidation.errorMessage {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.foregroundColor(.red)
|
|
.font(.caption)
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
|
|
Text("Path to the VibeTunnel web project directory containing package.json.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Development Server")
|
|
.font(.headline)
|
|
} footer: {
|
|
if useDevServer {
|
|
Text(
|
|
"Requires pnpm to be installed. The server will run 'pnpm run dev' with the same arguments as the built-in server."
|
|
)
|
|
.font(.caption)
|
|
.frame(maxWidth: .infinity)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func selectDirectory() {
|
|
let panel = NSOpenPanel()
|
|
panel.canChooseFiles = false
|
|
panel.canChooseDirectories = true
|
|
panel.allowsMultipleSelection = false
|
|
|
|
// Set initial directory
|
|
if !devServerPath.isEmpty {
|
|
let expandedPath = NSString(string: devServerPath).expandingTildeInPath
|
|
panel.directoryURL = URL(fileURLWithPath: expandedPath)
|
|
}
|
|
|
|
if panel.runModal() == .OK, let url = panel.url {
|
|
let path = url.path
|
|
let homeDir = NSHomeDirectory()
|
|
if path.hasPrefix(homeDir) {
|
|
devServerPath = "~" + path.dropFirst(homeDir.count)
|
|
} else {
|
|
devServerPath = path
|
|
}
|
|
|
|
// Validate immediately after selection
|
|
validateDevServer(devServerPath)
|
|
}
|
|
}
|
|
}
|