vibetunnel/tauri/src-tauri/public/settings.html
Peter Steinberger 2b5060e75f feat(tauri): Major refactoring with enhanced features and managers
- Add comprehensive manager system for various features:
  - Notification manager for in-app notifications
  - Permission manager for system permissions
  - Update manager for app updates
  - Backend manager for server backend management
  - Debug features manager for debugging tools
  - API testing manager for API test suites
  - Auth cache manager for credential caching
  - Terminal integrations manager for terminal emulator support
  - Session monitor for tracking active sessions
  - Port conflict resolver for port management
  - Network utilities for network information
  - TTY forward manager for TTY forwarding
  - Cast manager for terminal recording
  - App mover for macOS app location management
  - Terminal spawn service for launching terminals
  - File system API for file operations

- Add settings UI pages (settings.html, server-console.html)
- Update tauri.conf.json with new configuration
- Enhance server implementation with better state management
- Add comprehensive command system for all managers
- Update dependencies in Cargo.toml
- Add welcome screen manager for onboarding
- Implement proper state management across all components
2025-06-23 04:07:16 +02:00

1286 lines
No EOL
49 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VibeTunnel Settings</title>
<style>
:root {
/* Light mode colors */
--bg-color: #f5f5f7;
--window-bg: #ffffff;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--text-tertiary: #c7c7cc;
--accent-color: #007aff;
--accent-hover: #0051d5;
--border-color: rgba(0, 0, 0, 0.1);
--shadow-color: rgba(0, 0, 0, 0.1);
--tab-bg: #f2f2f7;
--tab-active-bg: #ffffff;
--input-bg: #ffffff;
--input-border: rgba(0, 0, 0, 0.15);
--success-color: #34c759;
--error-color: #ff3b30;
--warning-color: #ff9500;
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode colors */
--bg-color: #000000;
--window-bg: #1c1c1e;
--text-primary: #f5f5f7;
--text-secondary: #98989d;
--text-tertiary: #48484a;
--accent-color: #0a84ff;
--accent-hover: #409cff;
--border-color: rgba(255, 255, 255, 0.1);
--shadow-color: rgba(0, 0, 0, 0.5);
--tab-bg: #2c2c2e;
--tab-active-bg: #1c1c1e;
--input-bg: #2c2c2e;
--input-border: rgba(255, 255, 255, 0.15);
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
width: 100vw;
height: 100vh;
overflow: hidden;
-webkit-user-select: none;
user-select: none;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--window-bg);
}
/* Tab Bar */
.tab-bar {
display: flex;
background-color: var(--tab-bg);
border-bottom: 1px solid var(--border-color);
padding: 0;
height: 48px;
}
.tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 20px;
cursor: pointer;
transition: background-color 0.2s ease;
border-right: 1px solid var(--border-color);
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
position: relative;
}
.tab:last-child {
border-right: none;
}
.tab:hover {
background-color: var(--tab-active-bg);
}
.tab.active {
background-color: var(--tab-active-bg);
color: var(--text-primary);
}
.tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: var(--accent-color);
}
.tab-icon {
width: 16px;
height: 16px;
margin-right: 8px;
color: currentColor;
}
/* Content Area */
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
background-color: var(--window-bg);
}
.tab-content {
display: none;
animation: fadeIn 0.2s ease-out;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Form Styles */
.form-group {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: var(--text-primary);
}
.help-text {
font-size: 11px;
color: var(--text-secondary);
margin-top: 4px;
line-height: 1.4;
}
input[type="text"],
input[type="password"],
input[type="number"],
select {
width: 100%;
padding: 8px 12px;
font-size: 13px;
border: 1px solid var(--input-border);
border-radius: 6px;
background-color: var(--input-bg);
color: var(--text-primary);
transition: all 0.2s ease;
-webkit-user-select: text;
user-select: text;
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
}
input[type="checkbox"] {
width: 16px;
height: 16px;
margin-right: 8px;
cursor: pointer;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
font-size: 13px;
}
.button {
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
background-color: var(--accent-color);
color: white;
}
.button:hover {
background-color: var(--accent-hover);
}
.button:active {
transform: scale(0.98);
}
.button.secondary {
background-color: transparent;
color: var(--accent-color);
border: 1px solid var(--accent-color);
}
.button.secondary:hover {
background-color: var(--accent-color);
color: white;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Section Styles */
.section {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border-color);
}
.section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
}
/* Status Indicators */
.status {
display: inline-flex;
align-items: center;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
margin-left: 8px;
}
.status.success {
background-color: rgba(52, 199, 89, 0.1);
color: var(--success-color);
}
.status.error {
background-color: rgba(255, 59, 48, 0.1);
color: var(--error-color);
}
.status.warning {
background-color: rgba(255, 149, 0, 0.1);
color: var(--warning-color);
}
/* Button Group */
.button-group {
display: flex;
gap: 8px;
margin-top: 12px;
}
/* Info Box */
.info-box {
background-color: var(--tab-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
margin-bottom: 16px;
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
}
.info-box.warning {
background-color: rgba(255, 149, 0, 0.1);
border-color: var(--warning-color);
color: var(--text-primary);
}
/* Console */
.console {
background-color: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 6px;
padding: 12px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 11px;
line-height: 1.5;
max-height: 300px;
overflow-y: auto;
-webkit-user-select: text;
user-select: text;
}
.console-line {
margin-bottom: 4px;
}
.console-line.error {
color: var(--error-color);
}
.console-line.success {
color: var(--success-color);
}
/* Loading Shimmer */
.shimmer {
background: linear-gradient(
90deg,
var(--tab-bg) 0%,
var(--bg-color) 50%,
var(--tab-bg) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Permission Status */
.permission-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
.permission-item:last-child {
border-bottom: none;
}
.permission-name {
font-size: 13px;
font-weight: 500;
}
.permission-status {
display: flex;
align-items: center;
gap: 8px;
}
.permission-icon {
width: 16px;
height: 16px;
}
.permission-icon.granted {
color: var(--success-color);
}
.permission-icon.denied {
color: var(--error-color);
}
/* About Section */
.about-content {
text-align: center;
padding: 40px 0;
}
.app-icon-large {
width: 128px;
height: 128px;
margin-bottom: 20px;
border-radius: 27.6%;
filter: drop-shadow(0 4px 12px var(--shadow-color));
}
.app-name {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.app-version {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 24px;
}
.copyright {
font-size: 12px;
color: var(--text-tertiary);
margin-top: 32px;
}
/* IP Address Display */
.ip-display {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.ip-address {
font-family: 'SF Mono', Monaco, monospace;
font-size: 13px;
padding: 4px 8px;
background-color: var(--tab-bg);
border-radius: 4px;
-webkit-user-select: text;
user-select: text;
}
.copy-button {
padding: 4px 8px;
font-size: 11px;
background-color: transparent;
color: var(--accent-color);
border: 1px solid var(--accent-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.copy-button:hover {
background-color: var(--accent-color);
color: white;
}
/* Terminal List */
.terminal-list {
margin-top: 12px;
}
.terminal-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background-color: var(--tab-bg);
border-radius: 6px;
margin-bottom: 8px;
font-size: 13px;
}
.terminal-icon {
width: 24px;
height: 24px;
margin-right: 12px;
}
/* Loading Spinner */
.spinner {
width: 16px;
height: 16px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
margin-left: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<!-- Tab Bar -->
<div class="tab-bar">
<div class="tab active" data-tab="general">
<svg class="tab-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
General
</div>
<div class="tab" data-tab="dashboard">
<svg class="tab-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"></path>
</svg>
Dashboard
</div>
<div class="tab" data-tab="advanced">
<svg class="tab-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
</svg>
Advanced
</div>
<div class="tab" data-tab="debug" id="debugTab" style="display: none;">
<svg class="tab-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
</svg>
Debug
</div>
<div class="tab" data-tab="about">
<svg class="tab-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
About
</div>
</div>
<!-- Content Area -->
<div class="content">
<!-- General Tab -->
<div class="tab-content active" id="general">
<div class="section">
<h2 class="section-title">Startup</h2>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="launchAtLogin">
Launch VibeTunnel at login
</label>
<p class="help-text">Automatically start VibeTunnel when you log in to your computer</p>
</div>
</div>
<div class="section">
<h2 class="section-title">Updates</h2>
<div class="form-group">
<label for="updateChannel">Update Channel</label>
<select id="updateChannel">
<option value="stable">Stable Releases</option>
<option value="beta">Beta Releases</option>
<option value="nightly">Nightly Builds</option>
</select>
<p class="help-text">Choose which release channel to receive updates from</p>
</div>
<div class="form-group">
<button class="button" id="checkUpdates">Check for Updates</button>
<span id="updateStatus" class="status" style="display: none;"></span>
</div>
</div>
<div class="section">
<h2 class="section-title">System Permissions</h2>
<div class="info-box">
VibeTunnel requires certain permissions to function properly. Grant these permissions to enable all features.
</div>
<div id="permissionsList">
<!-- Permissions will be populated here -->
</div>
</div>
</div>
<!-- Dashboard Tab -->
<div class="tab-content" id="dashboard">
<div class="section">
<h2 class="section-title">Dashboard Security</h2>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="enablePassword">
Password protect the dashboard
</label>
<p class="help-text">Require a password to access the terminal dashboard</p>
</div>
<div class="form-group" id="passwordGroup" style="display: none;">
<label for="dashboardPassword">Dashboard Password</label>
<input type="password" id="dashboardPassword" placeholder="Enter password">
</div>
</div>
<div class="section">
<h2 class="section-title">Server Configuration</h2>
<div class="form-group">
<label for="serverPort">Server Port</label>
<input type="number" id="serverPort" min="1024" max="65535" placeholder="4020">
<p class="help-text">The port VibeTunnel's HTTP server will listen on</p>
</div>
<div class="form-group">
<label for="accessMode">Access Mode</label>
<select id="accessMode">
<option value="localhost">Localhost Only (127.0.0.1)</option>
<option value="network">Network Access (0.0.0.0)</option>
</select>
<p class="help-text">Control who can access your terminal dashboard</p>
</div>
<div id="networkInfo" style="display: none;">
<div class="form-group">
<label>Local IP Address</label>
<div class="ip-display">
<span class="ip-address" id="localIP">192.168.1.100</span>
<button class="copy-button" onclick="copyIP()">Copy</button>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">Remote Access</h2>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="enableNgrok">
Enable ngrok tunnel
</label>
<p class="help-text">Create a secure tunnel to access your terminals from anywhere</p>
</div>
<div id="ngrokConfig" style="display: none;">
<div class="form-group">
<label for="ngrokToken">ngrok Auth Token</label>
<input type="password" id="ngrokToken" placeholder="Enter your ngrok auth token">
<p class="help-text">Get your token from <a href="#" onclick="openNgrokDashboard()">ngrok.com</a></p>
</div>
<div class="form-group">
<label for="ngrokRegion">Region</label>
<select id="ngrokRegion">
<option value="us">United States</option>
<option value="eu">Europe</option>
<option value="ap">Asia Pacific</option>
<option value="au">Australia</option>
</select>
</div>
</div>
</div>
</div>
<!-- Advanced Tab -->
<div class="tab-content" id="advanced">
<div class="section">
<h2 class="section-title">Terminal Settings</h2>
<div class="form-group">
<label for="defaultTerminal">Default Terminal</label>
<select id="defaultTerminal">
<option value="system">System Default</option>
</select>
<p class="help-text">Choose your preferred terminal emulator</p>
</div>
<div class="form-group">
<label for="defaultShell">Default Shell</label>
<input type="text" id="defaultShell" placeholder="/bin/zsh">
<p class="help-text">Shell to use for new terminal sessions</p>
</div>
</div>
<div class="section">
<h2 class="section-title">CLI Tool</h2>
<div class="info-box">
The <code>vt</code> command lets you quickly create terminal sessions from your existing terminal.
</div>
<div class="form-group">
<div class="button-group">
<button class="button" id="installCLI">Install CLI</button>
<button class="button secondary" id="uninstallCLI">Uninstall</button>
</div>
<span id="cliStatus" class="status" style="display: none;"></span>
</div>
</div>
<div class="section">
<h2 class="section-title">Session Management</h2>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="cleanupOnStartup">
Clean up sessions on startup
</label>
<p class="help-text">Remove all terminal sessions when VibeTunnel starts</p>
</div>
<div class="form-group">
<label for="sessionTimeout">Session Timeout (minutes)</label>
<input type="number" id="sessionTimeout" min="0" placeholder="0">
<p class="help-text">Automatically close idle sessions (0 = disabled)</p>
</div>
</div>
<div class="section">
<h2 class="section-title">Display</h2>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="showInDock">
Show VibeTunnel in Dock
</label>
<p class="help-text">Display the VibeTunnel icon in the macOS Dock</p>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="debugMode">
Enable Debug Mode
</label>
<p class="help-text">Show debug options and additional logging</p>
</div>
</div>
</div>
<!-- Debug Tab -->
<div class="tab-content" id="debug">
<div class="section">
<h2 class="section-title">Server Status</h2>
<div class="info-box">
<div>Server: <span id="serverStatus">Running</span></div>
<div>Port: <span id="serverPortStatus">4020</span></div>
<div>Mode: <span id="serverModeStatus">Rust</span></div>
<div>Sessions: <span id="sessionCount">0</span></div>
</div>
<div class="button-group">
<button class="button" id="restartServer">Restart Server</button>
<button class="button secondary" id="stopServer">Stop Server</button>
</div>
</div>
<div class="section">
<h2 class="section-title">API Testing</h2>
<div class="form-group">
<label for="apiEndpoint">Test Endpoint</label>
<select id="apiEndpoint">
<option value="/health">Health Check</option>
<option value="/sessions">List Sessions</option>
<option value="/terminal/list">List Terminals</option>
</select>
</div>
<div class="form-group">
<button class="button" id="testAPI">Test API</button>
</div>
<div class="console" id="apiResponse" style="display: none;">
<!-- API response will be shown here -->
</div>
</div>
<div class="section">
<h2 class="section-title">Server Console</h2>
<div class="button-group">
<button class="button secondary" id="clearConsole">Clear</button>
<button class="button secondary" id="exportLogs">Export Logs</button>
</div>
<div class="console" id="serverConsole">
<div class="console-line">Server started on port 4020</div>
<div class="console-line success">Health check: OK</div>
<!-- More log lines will be added here -->
</div>
</div>
<div class="section">
<h2 class="section-title">Developer Tools</h2>
<div class="form-group">
<label for="logLevel">Log Level</label>
<select id="logLevel">
<option value="error">Error</option>
<option value="warn">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
<option value="trace">Trace</option>
</select>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="enableTelemetry">
Enable anonymous telemetry
</label>
<p class="help-text">Help improve VibeTunnel by sending anonymous usage data</p>
</div>
</div>
</div>
<!-- About Tab -->
<div class="tab-content" id="about">
<div class="about-content">
<img src="icon.png" alt="VibeTunnel" class="app-icon-large">
<h1 class="app-name">VibeTunnel</h1>
<p class="app-version">Version <span id="appVersion">1.0.0</span></p>
<div class="button-group" style="justify-content: center;">
<button class="button secondary" onclick="openWebsite()">Website</button>
<button class="button secondary" onclick="openGitHub()">GitHub</button>
<button class="button secondary" onclick="openChangelog()">Changelog</button>
</div>
<p class="copyright">
© 2024 VibeTunnel. All rights reserved.<br>
Built with ❤️ for developers
</p>
</div>
</div>
</div>
</div>
<script>
const { invoke } = window.__TAURI__.tauri;
const { open } = window.__TAURI__.shell;
const { appWindow } = window.__TAURI__.window;
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
// Remove active class from all tabs and contents
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
// Add active class to clicked tab and corresponding content
tab.classList.add('active');
const tabId = tab.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
// Load settings on startup
async function loadSettings() {
try {
const settings = await invoke('get_all_settings');
// General settings
document.getElementById('launchAtLogin').checked = settings.general?.launch_at_login || false;
document.getElementById('updateChannel').value = settings.general?.update_channel || 'stable';
// Dashboard settings
document.getElementById('enablePassword').checked = settings.dashboard?.password_enabled || false;
document.getElementById('dashboardPassword').value = settings.dashboard?.password || '';
document.getElementById('serverPort').value = settings.dashboard?.server_port || '4020';
document.getElementById('accessMode').value = settings.dashboard?.access_mode || 'localhost';
document.getElementById('enableNgrok').checked = settings.dashboard?.ngrok?.enabled || false;
document.getElementById('ngrokToken').value = settings.dashboard?.ngrok?.auth_token || '';
document.getElementById('ngrokRegion').value = settings.dashboard?.ngrok?.region || 'us';
// Advanced settings
document.getElementById('defaultTerminal').value = settings.advanced?.default_terminal || 'system';
document.getElementById('defaultShell').value = settings.advanced?.default_shell || '/bin/zsh';
document.getElementById('cleanupOnStartup').checked = settings.advanced?.cleanup_on_startup || false;
document.getElementById('sessionTimeout').value = settings.advanced?.session_timeout || 0;
document.getElementById('showInDock').checked = settings.advanced?.show_in_dock ?? true;
document.getElementById('debugMode').checked = settings.advanced?.debug_mode || false;
// Debug settings
document.getElementById('logLevel').value = settings.debug?.log_level || 'info';
document.getElementById('enableTelemetry').checked = settings.debug?.enable_telemetry || false;
// Update UI based on settings
updatePasswordVisibility();
updateNgrokVisibility();
updateNetworkInfo();
updateDebugTabVisibility();
// Load permissions
await loadPermissions();
// Load terminal list
await loadTerminals();
// Check CLI status
await checkCLIStatus();
// Get app version
const version = await invoke('get_app_version');
document.getElementById('appVersion').textContent = version;
} catch (error) {
console.error('Failed to load settings:', error);
}
}
// Save settings helper
async function saveSetting(section, key, value) {
try {
await invoke('update_setting', { section, key, value: JSON.stringify(value) });
} catch (error) {
console.error(`Failed to save ${section}.${key}:`, error);
}
}
// General tab handlers
document.getElementById('launchAtLogin').addEventListener('change', async (e) => {
await saveSetting('general', 'launch_at_login', e.target.checked);
if (e.target.checked) {
await invoke('enable_auto_launch');
} else {
await invoke('disable_auto_launch');
}
});
document.getElementById('updateChannel').addEventListener('change', async (e) => {
await saveSetting('general', 'update_channel', e.target.value);
});
document.getElementById('checkUpdates').addEventListener('click', async () => {
const button = document.getElementById('checkUpdates');
const status = document.getElementById('updateStatus');
button.disabled = true;
status.style.display = 'inline-flex';
status.className = 'status';
status.innerHTML = '<span class="spinner"></span> Checking...';
try {
const result = await invoke('check_for_updates');
if (result.available) {
status.className = 'status warning';
status.textContent = 'Update available!';
// Trigger update download
await invoke('download_update');
} else {
status.className = 'status success';
status.textContent = 'Up to date';
}
} catch (error) {
status.className = 'status error';
status.textContent = 'Check failed';
} finally {
button.disabled = false;
setTimeout(() => {
status.style.display = 'none';
}, 3000);
}
});
// Dashboard tab handlers
document.getElementById('enablePassword').addEventListener('change', async (e) => {
updatePasswordVisibility();
await saveSetting('dashboard', 'password_enabled', e.target.checked);
});
document.getElementById('dashboardPassword').addEventListener('blur', async (e) => {
if (e.target.value) {
await saveSetting('dashboard', 'password', e.target.value);
await invoke('set_dashboard_password', { password: e.target.value });
}
});
document.getElementById('serverPort').addEventListener('blur', async (e) => {
const port = parseInt(e.target.value);
if (port >= 1024 && port <= 65535) {
await saveSetting('dashboard', 'server_port', port);
// Restart server with new port
await invoke('restart_server_with_port', { port });
}
});
document.getElementById('accessMode').addEventListener('change', async (e) => {
await saveSetting('dashboard', 'access_mode', e.target.value);
updateNetworkInfo();
// Update server bind address
const bindAddress = e.target.value === 'localhost' ? '127.0.0.1' : '0.0.0.0';
await invoke('update_server_bind_address', { address: bindAddress });
});
document.getElementById('enableNgrok').addEventListener('change', async (e) => {
updateNgrokVisibility();
await saveSetting('dashboard', 'ngrok.enabled', e.target.checked);
if (e.target.checked) {
await invoke('start_ngrok');
} else {
await invoke('stop_ngrok');
}
});
// Advanced tab handlers
document.getElementById('cleanupOnStartup').addEventListener('change', async (e) => {
await saveSetting('advanced', 'cleanup_on_startup', e.target.checked);
});
document.getElementById('sessionTimeout').addEventListener('blur', async (e) => {
const timeout = parseInt(e.target.value) || 0;
await saveSetting('advanced', 'session_timeout', timeout);
});
document.getElementById('showInDock').addEventListener('change', async (e) => {
await saveSetting('advanced', 'show_in_dock', e.target.checked);
await invoke('set_dock_icon_visibility', { visible: e.target.checked });
});
document.getElementById('debugMode').addEventListener('change', async (e) => {
await saveSetting('advanced', 'debug_mode', e.target.checked);
updateDebugTabVisibility();
});
// CLI handlers
document.getElementById('installCLI').addEventListener('click', async () => {
const status = document.getElementById('cliStatus');
status.style.display = 'inline-flex';
status.className = 'status';
status.innerHTML = '<span class="spinner"></span> Installing...';
try {
await invoke('install_cli');
status.className = 'status success';
status.textContent = 'Installed';
await checkCLIStatus();
} catch (error) {
status.className = 'status error';
status.textContent = 'Failed';
}
setTimeout(() => {
status.style.display = 'none';
}, 3000);
});
document.getElementById('uninstallCLI').addEventListener('click', async () => {
const status = document.getElementById('cliStatus');
status.style.display = 'inline-flex';
status.className = 'status';
status.innerHTML = '<span class="spinner"></span> Uninstalling...';
try {
await invoke('uninstall_cli');
status.className = 'status success';
status.textContent = 'Uninstalled';
await checkCLIStatus();
} catch (error) {
status.className = 'status error';
status.textContent = 'Failed';
}
setTimeout(() => {
status.style.display = 'none';
}, 3000);
});
// Debug tab handlers
document.getElementById('logLevel').addEventListener('change', async (e) => {
await saveSetting('debug', 'log_level', e.target.value);
await invoke('set_log_level', { level: e.target.value });
});
document.getElementById('restartServer').addEventListener('click', async () => {
await invoke('restart_server');
});
document.getElementById('stopServer').addEventListener('click', async () => {
await invoke('stop_server');
});
document.getElementById('testAPI').addEventListener('click', async () => {
const endpoint = document.getElementById('apiEndpoint').value;
const response = document.getElementById('apiResponse');
response.style.display = 'block';
response.innerHTML = '<div class="console-line">Testing ' + endpoint + '...</div>';
try {
const result = await invoke('test_api_endpoint', { endpoint });
response.innerHTML += '<div class="console-line success">' + JSON.stringify(result, null, 2) + '</div>';
} catch (error) {
response.innerHTML += '<div class="console-line error">Error: ' + error + '</div>';
}
});
// Helper functions
function updatePasswordVisibility() {
const enabled = document.getElementById('enablePassword').checked;
document.getElementById('passwordGroup').style.display = enabled ? 'block' : 'none';
}
function updateNgrokVisibility() {
const enabled = document.getElementById('enableNgrok').checked;
document.getElementById('ngrokConfig').style.display = enabled ? 'block' : 'none';
}
function updateNetworkInfo() {
const mode = document.getElementById('accessMode').value;
document.getElementById('networkInfo').style.display = mode === 'network' ? 'block' : 'none';
if (mode === 'network') {
// Get local IP
invoke('get_local_ip').then(ip => {
document.getElementById('localIP').textContent = ip;
});
}
}
function updateDebugTabVisibility() {
const enabled = document.getElementById('debugMode').checked;
document.getElementById('debugTab').style.display = enabled ? 'flex' : 'none';
}
async function loadPermissions() {
try {
const permissions = await invoke('check_all_permissions');
const container = document.getElementById('permissionsList');
container.innerHTML = '';
for (const [name, status] of Object.entries(permissions)) {
const item = document.createElement('div');
item.className = 'permission-item';
const nameEl = document.createElement('div');
nameEl.className = 'permission-name';
nameEl.textContent = formatPermissionName(name);
const statusEl = document.createElement('div');
statusEl.className = 'permission-status';
if (status === 'granted') {
statusEl.innerHTML = `
<svg class="permission-icon granted" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
<span>Granted</span>
`;
} else {
statusEl.innerHTML = `
<svg class="permission-icon denied" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
<button class="button" onclick="requestPermission('${name}')">Grant</button>
`;
}
item.appendChild(nameEl);
item.appendChild(statusEl);
container.appendChild(item);
}
} catch (error) {
console.error('Failed to load permissions:', error);
}
}
async function loadTerminals() {
try {
const terminals = await invoke('detect_terminals');
const select = document.getElementById('defaultTerminal');
// Clear existing options except system default
select.innerHTML = '<option value="system">System Default</option>';
terminals.forEach(terminal => {
const option = document.createElement('option');
option.value = terminal.id;
option.textContent = terminal.name;
select.appendChild(option);
});
} catch (error) {
console.error('Failed to load terminals:', error);
}
}
async function checkCLIStatus() {
try {
const installed = await invoke('is_cli_installed');
const installBtn = document.getElementById('installCLI');
const uninstallBtn = document.getElementById('uninstallCLI');
installBtn.disabled = installed;
uninstallBtn.disabled = !installed;
} catch (error) {
console.error('Failed to check CLI status:', error);
}
}
function formatPermissionName(name) {
return name.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
}
async function requestPermission(permission) {
try {
await invoke('request_permission', { permission });
await loadPermissions();
} catch (error) {
console.error('Failed to request permission:', error);
}
}
function copyIP() {
const ip = document.getElementById('localIP').textContent;
navigator.clipboard.writeText(ip);
// Show feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 1500);
}
// External links
async function openWebsite() {
await open('https://vibetunnel.com');
}
async function openGitHub() {
await open('https://github.com/vibetunnel/vibetunnel');
}
async function openChangelog() {
await open('https://github.com/vibetunnel/vibetunnel/releases');
}
async function openNgrokDashboard() {
await open('https://dashboard.ngrok.com');
}
// Server console updates
let consoleLines = [];
async function updateServerConsole() {
try {
const logs = await invoke('get_server_logs', { limit: 100 });
const console = document.getElementById('serverConsole');
console.innerHTML = logs.map(log => {
let className = 'console-line';
if (log.level === 'error') className += ' error';
else if (log.level === 'success') className += ' success';
return `<div class="${className}">${log.message}</div>`;
}).join('');
console.scrollTop = console.scrollHeight;
} catch (error) {
console.error('Failed to update server console:', error);
}
}
document.getElementById('clearConsole').addEventListener('click', () => {
document.getElementById('serverConsole').innerHTML = '';
consoleLines = [];
});
document.getElementById('exportLogs').addEventListener('click', async () => {
try {
await invoke('export_logs');
} catch (error) {
console.error('Failed to export logs:', error);
}
});
// Update server status periodically
async function updateServerStatus() {
try {
const status = await invoke('get_server_status');
document.getElementById('serverStatus').textContent = status.running ? 'Running' : 'Stopped';
document.getElementById('serverPortStatus').textContent = status.port;
document.getElementById('serverModeStatus').textContent = status.mode;
document.getElementById('sessionCount').textContent = status.session_count;
} catch (error) {
console.error('Failed to update server status:', error);
}
}
// Initialize
loadSettings();
// Update server status every 5 seconds
setInterval(updateServerStatus, 5000);
// Update console if debug tab is active
setInterval(() => {
if (document.getElementById('debug').classList.contains('active')) {
updateServerConsole();
}
}, 1000);
</script>
</body>
</html>