mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
877 lines
No EOL
27 KiB
HTML
877 lines
No EOL
27 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 Menu</title>
|
||
<style>
|
||
:root {
|
||
--menu-width: 360px;
|
||
--header-gradient-start: #f8f9fa;
|
||
--header-gradient-end: #e9ecef;
|
||
--accent-color: #007aff;
|
||
--accent-hover: #0051d5;
|
||
--destructive-color: #ff3b30;
|
||
--success-color: #34c759;
|
||
--text-primary: #1d1d1f;
|
||
--text-secondary: #86868b;
|
||
--border-color: #d2d2d7;
|
||
--hover-bg: rgba(0, 122, 255, 0.08);
|
||
--focus-ring: rgba(0, 122, 255, 0.5);
|
||
--activity-color: #30d158;
|
||
--git-clean: #34c759;
|
||
--git-modified: #ff9500;
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--header-gradient-start: #2c2c2e;
|
||
--header-gradient-end: #1c1c1e;
|
||
--text-primary: #f2f2f7;
|
||
--text-secondary: #98989f;
|
||
--border-color: #38383a;
|
||
--hover-bg: rgba(0, 122, 255, 0.15);
|
||
--bg-primary: #1c1c1e;
|
||
--bg-secondary: #2c2c2e;
|
||
}
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
|
||
background: var(--bg-primary, white);
|
||
color: var(--text-primary);
|
||
user-select: none;
|
||
overflow: hidden;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.menu-container {
|
||
width: var(--menu-width);
|
||
background: var(--bg-primary, white);
|
||
border-radius: 10px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: 100vh;
|
||
}
|
||
|
||
/* Header Section */
|
||
.header {
|
||
background: linear-gradient(to bottom, var(--header-gradient-start), var(--header-gradient-end));
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.header-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.app-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.app-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.app-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.status-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 4px 8px;
|
||
border-radius: 20px;
|
||
font-size: 10px;
|
||
font-weight: 500;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.status-badge.running {
|
||
background: rgba(52, 199, 89, 0.1);
|
||
color: var(--success-color);
|
||
border: 0.5px solid rgba(52, 199, 89, 0.3);
|
||
}
|
||
|
||
.status-badge.stopped {
|
||
background: rgba(255, 59, 48, 0.1);
|
||
color: var(--destructive-color);
|
||
border: 0.5px solid rgba(255, 59, 48, 0.3);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.status-badge.stopped:hover {
|
||
opacity: 0.8;
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.status-indicator {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
}
|
||
|
||
.server-addresses {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.address-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.address-icon {
|
||
width: 14px;
|
||
text-align: center;
|
||
color: var(--success-color);
|
||
}
|
||
|
||
.address-label {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.address-link {
|
||
font-family: 'SF Mono', Consolas, monospace;
|
||
color: var(--success-color);
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
background: none;
|
||
border: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.address-link:hover {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* Session List */
|
||
.session-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
max-height: 600px;
|
||
}
|
||
|
||
.session-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.session-section-title {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
.session-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: background-color 0.15s ease;
|
||
margin-bottom: 4px;
|
||
position: relative;
|
||
}
|
||
|
||
.session-row:hover {
|
||
background: var(--hover-bg);
|
||
}
|
||
|
||
.session-row:focus {
|
||
outline: none;
|
||
box-shadow: 0 0 0 1px var(--focus-ring);
|
||
}
|
||
|
||
.activity-indicator {
|
||
position: relative;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.activity-glow {
|
||
position: absolute;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
opacity: 0.3;
|
||
filter: blur(2px);
|
||
}
|
||
|
||
.activity-dot {
|
||
width: 4px;
|
||
height: 4px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
position: relative;
|
||
left: 2px;
|
||
top: 2px;
|
||
}
|
||
|
||
.session-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.session-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.session-command {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.session-name {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.session-details {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.folder-button {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.15s ease;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.folder-button:hover {
|
||
background: rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.folder-path {
|
||
font-family: 'SF Mono', Consolas, monospace;
|
||
}
|
||
|
||
.git-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
background: rgba(52, 199, 89, 0.1);
|
||
color: var(--git-clean);
|
||
}
|
||
|
||
.git-info.modified {
|
||
background: rgba(255, 149, 0, 0.1);
|
||
color: var(--git-modified);
|
||
}
|
||
|
||
.duration {
|
||
color: var(--text-secondary);
|
||
opacity: 0.6;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.close-button {
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 59, 48, 0.1);
|
||
border: 0.5px solid rgba(255, 59, 48, 0.3);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.session-row:hover .close-button {
|
||
display: flex;
|
||
}
|
||
|
||
.session-row:hover .duration {
|
||
display: none;
|
||
}
|
||
|
||
.close-button:hover {
|
||
transform: translateY(-50%) scale(1.1);
|
||
}
|
||
|
||
.activity-status {
|
||
font-size: 10px;
|
||
color: var(--activity-color);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Empty State */
|
||
.empty-state {
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 48px;
|
||
opacity: 0.3;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.empty-subtitle {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Action Bar */
|
||
.action-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px;
|
||
border-top: 1px solid var(--border-color);
|
||
background: var(--bg-secondary, #f8f9fa);
|
||
}
|
||
|
||
.action-button {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: var(--text-primary);
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.action-button:hover {
|
||
background: var(--hover-bg);
|
||
}
|
||
|
||
.action-button.primary {
|
||
background: var(--accent-color);
|
||
color: white;
|
||
}
|
||
|
||
.action-button.primary:hover {
|
||
background: var(--accent-hover);
|
||
}
|
||
|
||
.divider {
|
||
flex: 1;
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.menu-container {
|
||
animation: fadeIn 0.2s ease-out;
|
||
}
|
||
|
||
/* Scrollbar */
|
||
.session-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.session-list::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.session-list::-webkit-scrollbar-thumb {
|
||
background: var(--border-color);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.session-list::-webkit-scrollbar-thumb:hover {
|
||
background: var(--text-secondary);
|
||
}
|
||
|
||
/* Loading State */
|
||
.loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.spinner {
|
||
width: 24px;
|
||
height: 24px;
|
||
border: 2px solid var(--border-color);
|
||
border-top-color: var(--accent-color);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="menu-container" id="menu">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// API communication with Tauri
|
||
const { invoke } = window.__TAURI__.core;
|
||
|
||
// State
|
||
let serverStatus = null;
|
||
let sessions = [];
|
||
let tailscaleStatus = null;
|
||
let gitRepositories = {};
|
||
|
||
// Initialize menu
|
||
async function init() {
|
||
try {
|
||
// Load initial data
|
||
await Promise.all([
|
||
loadServerStatus(),
|
||
loadSessions(),
|
||
loadTailscaleStatus()
|
||
]);
|
||
|
||
// Render menu
|
||
render();
|
||
|
||
// Set up event listeners
|
||
setupEventListeners();
|
||
|
||
// Start monitoring
|
||
startMonitoring();
|
||
} catch (error) {
|
||
console.error('Failed to initialize menu:', error);
|
||
showError();
|
||
}
|
||
}
|
||
|
||
async function loadServerStatus() {
|
||
serverStatus = await invoke('get_server_status');
|
||
}
|
||
|
||
async function loadSessions() {
|
||
sessions = await invoke('get_monitored_sessions');
|
||
|
||
// Load git info for each session
|
||
for (const session of sessions) {
|
||
if (session.cwd) {
|
||
try {
|
||
const gitInfo = await invoke('get_cached_git_repository', { path: session.cwd });
|
||
if (gitInfo) {
|
||
gitRepositories[session.cwd] = gitInfo;
|
||
}
|
||
} catch (e) {
|
||
// Ignore errors for sessions without git repos
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadTailscaleStatus() {
|
||
try {
|
||
tailscaleStatus = await invoke('get_tailscale_status');
|
||
} catch (e) {
|
||
tailscaleStatus = null;
|
||
}
|
||
}
|
||
|
||
function render() {
|
||
const menu = document.getElementById('menu');
|
||
|
||
const activeSessions = sessions.filter(s => s.is_active);
|
||
const idleSessions = sessions.filter(s => !s.is_active);
|
||
|
||
menu.innerHTML = `
|
||
${renderHeader()}
|
||
${renderSessionList(activeSessions, idleSessions)}
|
||
${renderActionBar()}
|
||
`;
|
||
}
|
||
|
||
function renderHeader() {
|
||
const addresses = [];
|
||
|
||
if (serverStatus?.running) {
|
||
addresses.push({
|
||
icon: '🖥',
|
||
label: 'Local:',
|
||
address: `127.0.0.1:${serverStatus.port}`
|
||
});
|
||
|
||
if (tailscaleStatus?.is_running && tailscaleStatus?.hostname) {
|
||
addresses.push({
|
||
icon: '🛡',
|
||
label: 'Tailscale:',
|
||
address: tailscaleStatus.hostname
|
||
});
|
||
}
|
||
}
|
||
|
||
return `
|
||
<div class="header">
|
||
<div class="header-content">
|
||
<div class="header-top">
|
||
<div class="app-info">
|
||
<img src="icon.png" class="app-icon" alt="VibeTunnel">
|
||
<div class="app-title">VibeTunnel</div>
|
||
</div>
|
||
<div class="status-badge ${serverStatus?.running ? 'running' : 'stopped'}"
|
||
onclick="${serverStatus?.running ? '' : 'restartServer()'}">
|
||
<div class="status-indicator"></div>
|
||
<span>${serverStatus?.running ? 'Running' : 'Stopped'}</span>
|
||
</div>
|
||
</div>
|
||
${addresses.length > 0 ? `
|
||
<div class="server-addresses">
|
||
${addresses.map(addr => `
|
||
<div class="address-row">
|
||
<span class="address-icon">${addr.icon}</span>
|
||
<span class="address-label">${addr.label}</span>
|
||
<button class="address-link" onclick="openAddress('${addr.address}')">
|
||
${addr.address}
|
||
</button>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderSessionList(activeSessions, idleSessions) {
|
||
if (sessions.length === 0) {
|
||
return `
|
||
<div class="session-list">
|
||
<div class="empty-state">
|
||
<div class="empty-icon">📂</div>
|
||
<div class="empty-title">No Active Sessions</div>
|
||
<div class="empty-subtitle">Create a new session to get started</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<div class="session-list">
|
||
${activeSessions.length > 0 ? `
|
||
<div class="session-section">
|
||
<div class="session-section-title">Active Sessions</div>
|
||
${activeSessions.map(session => renderSessionRow(session, true)).join('')}
|
||
</div>
|
||
` : ''}
|
||
${idleSessions.length > 0 ? `
|
||
<div class="session-section">
|
||
<div class="session-section-title">Idle Sessions</div>
|
||
${idleSessions.map(session => renderSessionRow(session, false)).join('')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderSessionRow(session, isActive) {
|
||
const gitInfo = gitRepositories[session.cwd];
|
||
const hasChanges = gitInfo && gitInfo.has_changes;
|
||
|
||
return `
|
||
<div class="session-row" tabindex="0" onclick="openSession('${session.id}')"
|
||
onkeydown="handleSessionKeyDown(event, '${session.id}')">
|
||
<div class="activity-indicator" style="color: ${isActive ? 'var(--activity-color)' : 'var(--git-clean)'}">
|
||
<div class="activity-glow"></div>
|
||
<div class="activity-dot"></div>
|
||
</div>
|
||
<div class="session-info">
|
||
<div class="session-header">
|
||
<span class="session-command">${getCommandName(session)}</span>
|
||
${session.name ? `
|
||
<span class="session-name">– ${session.name}</span>
|
||
` : ''}
|
||
</div>
|
||
<div class="session-details">
|
||
<button class="folder-button" onclick="openFolder('${session.cwd}'); event.stopPropagation();">
|
||
<span>📁</span>
|
||
<span class="folder-path">${compactPath(session.cwd)}</span>
|
||
</button>
|
||
${gitInfo ? `
|
||
<div class="git-info ${hasChanges ? 'modified' : ''}">
|
||
<span>${gitInfo.current_branch || 'main'}</span>
|
||
${hasChanges ? `<span>(${getGitStatusText(gitInfo)})</span>` : ''}
|
||
</div>
|
||
` : ''}
|
||
<span class="duration">${formatDuration(session.created_at)}</span>
|
||
</div>
|
||
${session.last_activity ? `
|
||
<div class="activity-status">${session.last_activity}</div>
|
||
` : ''}
|
||
</div>
|
||
<button class="close-button" onclick="terminateSession('${session.id}'); event.stopPropagation();">
|
||
<span style="font-size: 8px;">✕</span>
|
||
</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderActionBar() {
|
||
return `
|
||
<div class="action-bar">
|
||
<button class="action-button primary" onclick="createNewSession()">
|
||
<span>+</span>
|
||
<span>New Session</span>
|
||
</button>
|
||
<div class="divider"></div>
|
||
<button class="action-button" onclick="openSettings()">
|
||
<span>⚙️</span>
|
||
<span>Settings</span>
|
||
</button>
|
||
<button class="action-button" onclick="quitApp()">
|
||
<span>Quit</span>
|
||
</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Helper functions
|
||
function getCommandName(session) {
|
||
// Extract command name from the full command
|
||
if (!session.shell || session.shell.length === 0) {
|
||
return 'Terminal';
|
||
}
|
||
const parts = session.shell.split('/');
|
||
return parts[parts.length - 1];
|
||
}
|
||
|
||
function compactPath(path) {
|
||
// Get the appropriate home directory based on the platform
|
||
const platform = navigator.platform.toLowerCase();
|
||
let homePatterns = [];
|
||
|
||
if (platform.includes('win')) {
|
||
// Windows: C:\Users\username or similar
|
||
homePatterns = [
|
||
/^[A-Za-z]:\\Users\\[^\\]+/,
|
||
/^[A-Za-z]:\\Documents and Settings\\[^\\]+/
|
||
];
|
||
} else if (platform.includes('mac')) {
|
||
// macOS: /Users/username
|
||
homePatterns = [/^\/Users\/[^\/]+/];
|
||
} else {
|
||
// Linux/Unix: /home/username
|
||
homePatterns = [/^\/home\/[^\/]+/];
|
||
}
|
||
|
||
// Try to match and replace home directory with ~
|
||
for (const pattern of homePatterns) {
|
||
const match = path.match(pattern);
|
||
if (match) {
|
||
return '~' + path.substring(match[0].length);
|
||
}
|
||
}
|
||
|
||
// Fallback: shorten long paths
|
||
const separator = path.includes('\\') ? '\\' : '/';
|
||
const parts = path.split(separator);
|
||
if (parts.length > 3) {
|
||
return '...' + separator + parts.slice(-2).join(separator);
|
||
}
|
||
return path;
|
||
}
|
||
|
||
function formatDuration(createdAt) {
|
||
const start = new Date(createdAt);
|
||
const now = new Date();
|
||
const elapsed = (now - start) / 1000; // seconds
|
||
|
||
if (elapsed < 60) return 'now';
|
||
if (elapsed < 3600) return `${Math.floor(elapsed / 60)}m`;
|
||
if (elapsed < 86400) return `${Math.floor(elapsed / 3600)}h`;
|
||
return `${Math.floor(elapsed / 86400)}d`;
|
||
}
|
||
|
||
function getGitStatusText(gitInfo) {
|
||
const parts = [];
|
||
if (gitInfo.modified_count > 0) parts.push(`${gitInfo.modified_count}M`);
|
||
if (gitInfo.added_count > 0) parts.push(`${gitInfo.added_count}+`);
|
||
if (gitInfo.deleted_count > 0) parts.push(`${gitInfo.deleted_count}-`);
|
||
if (gitInfo.untracked_count > 0) parts.push(`${gitInfo.untracked_count}?`);
|
||
return parts.join(' ');
|
||
}
|
||
|
||
// Event handlers
|
||
async function openSession(sessionId) {
|
||
try {
|
||
// First try to focus terminal window
|
||
await invoke('focus_terminal_window', { sessionId });
|
||
} catch (e) {
|
||
// If no window, open in browser
|
||
await invoke('open_dashboard');
|
||
}
|
||
}
|
||
|
||
async function terminateSession(sessionId) {
|
||
try {
|
||
// TODO: Implement session termination
|
||
console.log('Terminate session:', sessionId);
|
||
} catch (error) {
|
||
console.error('Failed to terminate session:', error);
|
||
}
|
||
}
|
||
|
||
async function openFolder(path) {
|
||
try {
|
||
await invoke('open_folder', { path });
|
||
} catch (error) {
|
||
console.error('Failed to open folder:', error);
|
||
}
|
||
}
|
||
|
||
async function openAddress(address) {
|
||
if (address.includes('127.0.0.1')) {
|
||
await invoke('open_dashboard');
|
||
} else {
|
||
// Open other addresses directly
|
||
window.open(`http://${address}`, '_blank');
|
||
}
|
||
}
|
||
|
||
async function restartServer() {
|
||
try {
|
||
await invoke('restart_server');
|
||
await loadServerStatus();
|
||
render();
|
||
} catch (error) {
|
||
console.error('Failed to restart server:', error);
|
||
}
|
||
}
|
||
|
||
async function createNewSession() {
|
||
// This would open a new session form
|
||
// For now, just open the dashboard
|
||
await invoke('open_dashboard');
|
||
}
|
||
|
||
async function openSettings() {
|
||
await invoke('open_settings_window');
|
||
}
|
||
|
||
async function quitApp() {
|
||
await invoke('quit_app');
|
||
}
|
||
|
||
function handleSessionKeyDown(event, sessionId) {
|
||
if (event.key === 'Enter' || event.key === ' ') {
|
||
event.preventDefault();
|
||
openSession(sessionId);
|
||
}
|
||
}
|
||
|
||
function setupEventListeners() {
|
||
// Listen for Tauri events
|
||
window.__TAURI__.event.listen('server-status-changed', async () => {
|
||
await loadServerStatus();
|
||
render();
|
||
});
|
||
|
||
window.__TAURI__.event.listen('sessions-changed', async () => {
|
||
await loadSessions();
|
||
render();
|
||
});
|
||
|
||
// Keyboard navigation
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
window.close();
|
||
}
|
||
});
|
||
}
|
||
|
||
function startMonitoring() {
|
||
// Refresh data periodically
|
||
setInterval(async () => {
|
||
await loadSessions();
|
||
render();
|
||
}, 5000);
|
||
}
|
||
|
||
function showError() {
|
||
const menu = document.getElementById('menu');
|
||
menu.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-icon">⚠️</div>
|
||
<div class="empty-title">Connection Error</div>
|
||
<div class="empty-subtitle">Failed to connect to VibeTunnel service</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Initialize when DOM is ready
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
</script>
|
||
</body>
|
||
</html> |