@burdenoff/microfe-vibecontrols 2026.623.1 → 2026.623.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/actions/ActionCard.js +2 -2
- package/dist/components/actions/ActionCard.js.map +1 -1
- package/dist/components/actions/ActionTypeBadge.js +1 -1
- package/dist/components/actions/ActionTypeBadge.js.map +1 -1
- package/dist/components/actions/ExecutionHistory.js +1 -1
- package/dist/components/actions/ExecutionHistory.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerAdminBar.js +1 -1
- package/dist/components/agent-manager/AgentManagerAdminBar.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerChatPane.js +2 -2
- package/dist/components/agent-manager/AgentManagerChatPane.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerInputArea.js +4 -4
- package/dist/components/agent-manager/AgentManagerInputArea.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerLayout.js +1 -1
- package/dist/components/agent-manager/AgentManagerLayout.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerMessageItem.js +1 -1
- package/dist/components/agent-manager/AgentManagerMessageItem.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerSidebar.js +5 -5
- package/dist/components/agent-manager/AgentManagerSidebar.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerTab.js +1 -1
- package/dist/components/agent-manager/AgentManagerTab.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerTabBar.js +1 -1
- package/dist/components/agent-manager/AgentManagerTabBar.js.map +1 -1
- package/dist/components/agent-manager/BridleConfigPanel.js +24 -24
- package/dist/components/agent-manager/BridleConfigPanel.js.map +1 -1
- package/dist/components/agent-manager/PluginInstallModal.js +1 -1
- package/dist/components/agent-manager/PluginInstallModal.js.map +1 -1
- package/dist/components/agent-manager/ProfilesPanel.js +5 -5
- package/dist/components/agent-manager/ProfilesPanel.js.map +1 -1
- package/dist/components/agent-manager/controls/AutonomySelector.js +1 -1
- package/dist/components/agent-manager/controls/AutonomySelector.js.map +1 -1
- package/dist/components/agent-manager/controls/SdkSelector.js +1 -1
- package/dist/components/agent-manager/controls/SdkSelector.js.map +1 -1
- package/dist/components/agent-manager/controls/VoiceToggle.js +1 -1
- package/dist/components/agent-manager/controls/VoiceToggle.js.map +1 -1
- package/dist/components/agent-manager/detail/SessionConfigPanel.js +6 -6
- package/dist/components/agent-manager/detail/SessionConfigPanel.js.map +1 -1
- package/dist/components/agent-manager/detail/SessionLogsPanel.js +5 -5
- package/dist/components/agent-manager/detail/SessionLogsPanel.js.map +1 -1
- package/dist/components/agent-manager/detail/SessionToolsPanel.js +1 -1
- package/dist/components/agent-manager/detail/SessionToolsPanel.js.map +1 -1
- package/dist/components/agent-manager/dialogs/NewSessionDialog.js +2 -2
- package/dist/components/agent-manager/dialogs/NewSessionDialog.js.map +1 -1
- package/dist/components/agent-manager/dialogs/RenameSessionDialog.js +1 -1
- package/dist/components/agent-manager/dialogs/RenameSessionDialog.js.map +1 -1
- package/dist/components/agents/AgentCard.js +14 -11
- package/dist/components/agents/AgentCard.js.map +1 -1
- package/dist/components/ai/AIContextsTab.js +1 -1
- package/dist/components/ai/AIContextsTab.js.map +1 -1
- package/dist/components/ai/AIOverviewTab.js +4 -4
- package/dist/components/ai/AIOverviewTab.js.map +1 -1
- package/dist/components/ai/AIPlanPanel.js +4 -4
- package/dist/components/ai/AIPlanPanel.js.map +1 -1
- package/dist/components/ai/AIPlaygroundTab.js +1 -1
- package/dist/components/ai/AIPlaygroundTab.js.map +1 -1
- package/dist/components/ai/AIPromptsTab.js +2 -2
- package/dist/components/ai/AIPromptsTab.js.map +1 -1
- package/dist/components/ai/AIProviderCredentialsCard.js +4 -4
- package/dist/components/ai/AIProviderCredentialsCard.js.map +1 -1
- package/dist/components/ai/AIQueueTab.js +4 -4
- package/dist/components/ai/AIQueueTab.js.map +1 -1
- package/dist/components/ai/AISessionsTab.js +4 -4
- package/dist/components/ai/AISessionsTab.js.map +1 -1
- package/dist/components/ai/AITasksTab.js +1 -1
- package/dist/components/ai/AITasksTab.js.map +1 -1
- package/dist/components/ai/ClaudeCredentialsCard.js +3 -3
- package/dist/components/ai/ClaudeCredentialsCard.js.map +1 -1
- package/dist/components/ai/PluginHarnessPicker.js +3 -3
- package/dist/components/ai/PluginHarnessPicker.js.map +1 -1
- package/dist/components/analytics/AnalyticsBreakdownChart.js +8 -8
- package/dist/components/analytics/AnalyticsBreakdownChart.js.map +1 -1
- package/dist/components/analytics/AnalyticsTimeSeriesChart.js +5 -5
- package/dist/components/analytics/AnalyticsTimeSeriesChart.js.map +1 -1
- package/dist/components/audit/AuditDeleteDialog.js +2 -2
- package/dist/components/audit/AuditDeleteDialog.js.map +1 -1
- package/dist/components/audit/AuditExportButton.js +5 -5
- package/dist/components/audit/AuditExportButton.js.map +1 -1
- package/dist/components/audit/AuditLogDetail.js +9 -9
- package/dist/components/audit/AuditLogDetail.js.map +1 -1
- package/dist/components/audit/AuditLogFilters.js +1 -1
- package/dist/components/audit/AuditLogFilters.js.map +1 -1
- package/dist/components/audit/AuditLogStats.js +1 -1
- package/dist/components/audit/AuditLogStats.js.map +1 -1
- package/dist/components/audit/AuditLogTable.js +28 -28
- package/dist/components/audit/AuditLogTable.js.map +1 -1
- package/dist/components/catalog/CatalogCard.js +3 -3
- package/dist/components/catalog/CatalogCard.js.map +1 -1
- package/dist/components/catalog/CatalogForm.js +1 -1
- package/dist/components/catalog/CatalogForm.js.map +1 -1
- package/dist/components/common/ActionAgentTargetNotice.js +6 -6
- package/dist/components/common/ActionAgentTargetNotice.js.map +1 -1
- package/dist/components/common/CommandEditor.js +1 -1
- package/dist/components/common/CommandEditor.js.map +1 -1
- package/dist/components/docs/DocsPageTree.js +1 -1
- package/dist/components/docs/DocsPageTree.js.map +1 -1
- package/dist/components/docs/DocsPreviewPanel.js +1 -1
- package/dist/components/docs/DocsPreviewPanel.js.map +1 -1
- package/dist/components/docs/DocsRevisionHistory.js +4 -4
- package/dist/components/docs/DocsRevisionHistory.js.map +1 -1
- package/dist/components/docs/DocsSettingsPanel.js +7 -7
- package/dist/components/docs/DocsSettingsPanel.js.map +1 -1
- package/dist/components/docs/DocsTabPanel.js +1 -1
- package/dist/components/docs/DocsTabPanel.js.map +1 -1
- package/dist/components/graph/ConnectionPermissionsDialog.js +2 -2
- package/dist/components/graph/ConnectionPermissionsDialog.js.map +1 -1
- package/dist/components/graph/MeshDeployWizard.js +3 -3
- package/dist/components/graph/MeshDeployWizard.js.map +1 -1
- package/dist/components/graph/MeshRequestPanel.js +2 -2
- package/dist/components/graph/MeshRequestPanel.js.map +1 -1
- package/dist/components/idp-components/ComponentCard.js +5 -5
- package/dist/components/idp-components/ComponentCard.js.map +1 -1
- package/dist/components/idp-components/ComponentForm.js +1 -1
- package/dist/components/idp-components/ComponentForm.js.map +1 -1
- package/dist/components/idp-components/DependencyGraphTab.js +7 -7
- package/dist/components/idp-components/DependencyGraphTab.js.map +1 -1
- package/dist/components/logs/LogViewer.js +14 -14
- package/dist/components/logs/LogViewer.js.map +1 -1
- package/dist/components/notes/NoteCard.js +1 -1
- package/dist/components/notes/NoteCard.js.map +1 -1
- package/dist/components/notes/NoteEditor.js +2 -2
- package/dist/components/notes/NoteEditor.js.map +1 -1
- package/dist/components/notes/NoteList.js +1 -1
- package/dist/components/notes/NoteList.js.map +1 -1
- package/dist/components/notes/NotesTabPanel.js +1 -1
- package/dist/components/notes/NotesTabPanel.js.map +1 -1
- package/dist/components/scorecard/ScorecardEmptyState.js +1 -1
- package/dist/components/scorecard/ScorecardEmptyState.js.map +1 -1
- package/dist/components/scorecard/ScorecardPanel.js +3 -3
- package/dist/components/scorecard/ScorecardPanel.js.map +1 -1
- package/dist/components/security/SecurityScanList.js +11 -11
- package/dist/components/security/SecurityScanList.js.map +1 -1
- package/dist/components/sessions/SessionCard.js +26 -19
- package/dist/components/sessions/SessionCard.js.map +1 -1
- package/dist/components/sessions/SessionStatusBadge.js +7 -7
- package/dist/components/sessions/SessionStatusBadge.js.map +1 -1
- package/dist/components/shared/AgentSelector.js +4 -4
- package/dist/components/shared/AgentSelector.js.map +1 -1
- package/dist/components/shared/AgentSetupBanner.js +4 -4
- package/dist/components/shared/AgentSetupBanner.js.map +1 -1
- package/dist/components/shared/Card.js +4 -4
- package/dist/components/shared/Card.js.map +1 -1
- package/dist/components/shared/ErrorState.js +15 -9
- package/dist/components/shared/ErrorState.js.map +1 -1
- package/dist/components/shared/FilterDropdown.js +2 -2
- package/dist/components/shared/FilterDropdown.js.map +1 -1
- package/dist/components/shared/PageSkeleton.js +6 -6
- package/dist/components/shared/PageSkeleton.js.map +1 -1
- package/dist/components/shared/SearchInput.js +1 -1
- package/dist/components/shared/SearchInput.js.map +1 -1
- package/dist/components/shared/StatCard.js +7 -7
- package/dist/components/shared/StatCard.js.map +1 -1
- package/dist/components/templates/PublishTemplateForm.js +2 -2
- package/dist/components/templates/PublishTemplateForm.js.map +1 -1
- package/dist/components/templates/TemplateCard.js +5 -5
- package/dist/components/templates/TemplateCard.js.map +1 -1
- package/dist/components/templates/TemplateWizard.js +2 -2
- package/dist/components/templates/TemplateWizard.js.map +1 -1
- package/dist/components/tunnels/DomainVerificationBadge.js +4 -4
- package/dist/components/tunnels/DomainVerificationBadge.js.map +1 -1
- package/dist/components/tunnels/ProviderBadge.js +1 -1
- package/dist/components/tunnels/ProviderBadge.js.map +1 -1
- package/dist/components/tunnels/TunnelCard.js +16 -16
- package/dist/components/tunnels/TunnelCard.js.map +1 -1
- package/dist/components/tunnels/TunnelCreateWizard.js +1 -1
- package/dist/components/tunnels/TunnelCreateWizard.js.map +1 -1
- package/dist/components/tunnels/TunnelPublicAccessPanel.js +1 -1
- package/dist/components/tunnels/TunnelPublicAccessPanel.js.map +1 -1
- package/dist/components/tunnels/TunnelStatusBadge.js +4 -4
- package/dist/components/tunnels/TunnelStatusBadge.js.map +1 -1
- package/dist/components/vibecalendar/LogsDrawer.js +2 -2
- package/dist/components/vibecalendar/LogsDrawer.js.map +1 -1
- package/dist/components/vibecalendar/ScheduleTaskModal.js +5 -5
- package/dist/components/vibecalendar/ScheduleTaskModal.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalAgendaView.js +2 -2
- package/dist/components/vibecalendar/VibeCalAgendaView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalDayView.js +3 -3
- package/dist/components/vibecalendar/VibeCalDayView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalMonthView.js +1 -1
- package/dist/components/vibecalendar/VibeCalMonthView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalWeekView.js +4 -4
- package/dist/components/vibecalendar/VibeCalWeekView.js.map +1 -1
- package/dist/components/vibecalendar/VibeCalendarHeader.js +4 -4
- package/dist/components/vibecalendar/VibeCalendarHeader.js.map +1 -1
- package/dist/components/vibecalendar/VibeTaskBlock.js +2 -2
- package/dist/components/vibecalendar/VibeTaskBlock.js.map +1 -1
- package/dist/components/vibecalendar/types.js +7 -7
- package/dist/components/vibecalendar/types.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckButtonForm.js +9 -9
- package/dist/components/vibedeck/VibeDeckButtonForm.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckButtonTile.js +6 -6
- package/dist/components/vibedeck/VibeDeckButtonTile.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckExecutionLog.js +10 -10
- package/dist/components/vibedeck/VibeDeckExecutionLog.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckForm.js +8 -8
- package/dist/components/vibedeck/VibeDeckForm.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckGrid.js +2 -2
- package/dist/components/vibedeck/VibeDeckGrid.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckPublicLinkPanel.js +2 -2
- package/dist/components/vibedeck/VibeDeckPublicLinkPanel.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckSwitcher.js +8 -8
- package/dist/components/vibedeck/VibeDeckSwitcher.js.map +1 -1
- package/dist/components/vibes/VibeCard.js +22 -18
- package/dist/components/vibes/VibeCard.js.map +1 -1
- package/dist/components/vibes/VibeList.js +16 -12
- package/dist/components/vibes/VibeList.js.map +1 -1
- package/dist/pages/ActionsPage.js +4 -3
- package/dist/pages/ActionsPage.js.map +1 -1
- package/dist/pages/AgentDetailsPage.js +9 -9
- package/dist/pages/AgentDetailsPage.js.map +1 -1
- package/dist/pages/AgentsLandingPage.js +4 -4
- package/dist/pages/AgentsLandingPage.js.map +1 -1
- package/dist/pages/AgentsPage.js +14 -14
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/pages/AnalyticsPage.js +8 -8
- package/dist/pages/AnalyticsPage.js.map +1 -1
- package/dist/pages/CatalogDetailPage.js +11 -11
- package/dist/pages/CatalogDetailPage.js.map +1 -1
- package/dist/pages/CatalogPage.js +12 -9
- package/dist/pages/CatalogPage.js.map +1 -1
- package/dist/pages/ComponentDetailPage.js +7 -7
- package/dist/pages/ComponentDetailPage.js.map +1 -1
- package/dist/pages/ComponentsPage.js +12 -9
- package/dist/pages/ComponentsPage.js.map +1 -1
- package/dist/pages/EnvTopologyPage.js +2 -2
- package/dist/pages/EnvTopologyPage.js.map +1 -1
- package/dist/pages/GettingStartedPage.js +12 -12
- package/dist/pages/GettingStartedPage.js.map +1 -1
- package/dist/pages/HealthPage.js +12 -12
- package/dist/pages/HealthPage.js.map +1 -1
- package/dist/pages/LogsPage.js +4 -4
- package/dist/pages/LogsPage.js.map +1 -1
- package/dist/pages/NoteDetailsPage.js +5 -5
- package/dist/pages/NoteDetailsPage.js.map +1 -1
- package/dist/pages/OverviewPage.js +76 -43
- package/dist/pages/OverviewPage.js.map +1 -1
- package/dist/pages/PluginsCatalogPage.js +7 -4
- package/dist/pages/PluginsCatalogPage.js.map +1 -1
- package/dist/pages/ScheduledJobDetailPage.js +7 -7
- package/dist/pages/ScheduledJobDetailPage.js.map +1 -1
- package/dist/pages/SessionDetailsPage.js +21 -21
- package/dist/pages/SessionDetailsPage.js.map +1 -1
- package/dist/pages/SessionsPage.js +20 -20
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/pages/SettingsPage.js +64 -52
- package/dist/pages/SettingsPage.js.map +1 -1
- package/dist/pages/SharedSessionPage.js +21 -21
- package/dist/pages/SharedSessionPage.js.map +1 -1
- package/dist/pages/SharedVibeDeckPage.js +5 -5
- package/dist/pages/SharedVibeDeckPage.js.map +1 -1
- package/dist/pages/TargetDetailsPage.js +56 -65
- package/dist/pages/TargetDetailsPage.js.map +1 -1
- package/dist/pages/TargetsPage.js +4 -4
- package/dist/pages/TargetsPage.js.map +1 -1
- package/dist/pages/TemplateDetailPage.js +10 -10
- package/dist/pages/TemplateDetailPage.js.map +1 -1
- package/dist/pages/TemplatesPage.js +6 -5
- package/dist/pages/TemplatesPage.js.map +1 -1
- package/dist/pages/VibeAuditPage.js +3 -3
- package/dist/pages/VibeAuditPage.js.map +1 -1
- package/dist/pages/VibeCalendarPage.js +3 -3
- package/dist/pages/VibeCalendarPage.js.map +1 -1
- package/dist/pages/VibeDeckPage.js +3 -3
- package/dist/pages/VibeDeckPage.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js +15 -15
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/pages/VibeTaskDetailPage.js +5 -5
- package/dist/pages/VibeTaskDetailPage.js.map +1 -1
- package/dist/pages/VibesPage.js +18 -18
- package/dist/pages/VibesPage.js.map +1 -1
- package/dist/pages/WebhooksPage.js +11 -11
- package/dist/pages/WebhooksPage.js.map +1 -1
- package/dist/pages/ai/AIBookmarksPage.js +1 -1
- package/dist/pages/ai/AIBookmarksPage.js.map +1 -1
- package/dist/pages/ai/AISessionDetailPage.js +6 -6
- package/dist/pages/ai/AISessionDetailPage.js.map +1 -1
- package/dist/pages/tunnels/AuditTab.js +1 -1
- package/dist/pages/tunnels/AuditTab.js.map +1 -1
- package/dist/pages/tunnels/DomainsTab.js +1 -1
- package/dist/pages/tunnels/DomainsTab.js.map +1 -1
- package/dist/pages/tunnels/ProvidersTab.js +1 -1
- package/dist/pages/tunnels/ProvidersTab.js.map +1 -1
- package/dist/pages/tunnels/SessionsTab.js +1 -1
- package/dist/pages/tunnels/SessionsTab.js.map +1 -1
- package/dist/pages/tunnels/TunnelDetailsPage.js +3 -3
- package/dist/pages/tunnels/TunnelDetailsPage.js.map +1 -1
- package/dist/pages/tunnels/UsageTab.js +1 -1
- package/dist/pages/tunnels/UsageTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js +16 -16
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelOverviewTab.js +1 -1
- package/dist/pages/tunnels/tabs/TunnelOverviewTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelSessionsTab.js +52 -49
- package/dist/pages/tunnels/tabs/TunnelSessionsTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelSettingsTab.js +9 -9
- package/dist/pages/tunnels/tabs/TunnelSettingsTab.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AISessionDetailPage.js","names":[],"sources":["../../../src/pages/ai/AISessionDetailPage.tsx"],"sourcesContent":["import { useState, useEffect, useCallback, useMemo } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n MessageSquare,\n ScrollText,\n BarChart3,\n Settings,\n Send,\n ArrowLeft,\n XCircle,\n RefreshCw,\n} from 'lucide-react';\nimport { MotionPage } from '@burdenoff/fe-libs/shared/motion';\nimport {\n PageHeader,\n Card,\n LoadingState,\n ErrorState,\n EmptyState,\n StatCard,\n RequireAgentReachable,\n} from '@/components/shared';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport { useQueryBackedTab } from '@/hooks/useQueryBackedTab';\nimport { useCachedInitialData } from '@/hooks/useCachedInitialData';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport type { AISessionLog, AISessionStats } from '@/services/aiApi';\nimport { AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n sendPromptToSession,\n fetchSessionLogs,\n fetchSessionStats,\n updateSessionConfig,\n terminateSession,\n} from '@/services/aiApi';\nimport { isSessionNotFoundError } from '@/utils/aiSessionNotFound';\n\ntype SessionTab = 'chat' | 'logs' | 'stats' | 'config';\nconst SESSION_TABS: readonly SessionTab[] = ['chat', 'logs', 'stats', 'config'];\n\nconst sessionTabs: { id: SessionTab; label: string; icon: typeof MessageSquare }[] = [\n { id: 'chat', label: 'Chat', icon: MessageSquare },\n { id: 'logs', label: 'Logs', icon: ScrollText },\n { id: 'stats', label: 'Stats', icon: BarChart3 },\n { id: 'config', label: 'Config', icon: Settings },\n];\n\nconst logTypeColors: Record<string, string> = {\n info: 'bg-action-primary-bg/10 text-action-primary-bg',\n warn: 'bg-status-warning-bg text-status-warning-text',\n error: 'bg-status-error-bg text-status-error-text',\n debug: 'bg-bg-sunken text-text-secondary',\n prompt: 'bg-status-success-bg text-status-success-text',\n response: 'bg-action-primary-bg/20 text-action-primary-bg',\n};\n\ninterface ChatMessage {\n role: 'user' | 'assistant';\n content: string;\n tokens?: { input: number; output: number };\n timestamp: string;\n}\n\n/**\n * AI session detail page with Chat, Logs, Stats, and Config tabs.\n */\nexport function AISessionDetailPage() {\n const tr = useTr();\n // Agent-centric page — gate on `agent:read` (defense-in-depth).\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const canReadAgents = hasPermission(VIBE_PERMISSIONS.AGENT_READ);\n const { sessionId } = useParams<{ sessionId: string }>();\n const handleNavigate = useVibeNavigate();\n\n const { agents, loading: agentsLoading } = useCachedInitialData();\n const activeAgent = useMemo(\n () => agents.find((agent) => agent.isActive && agent.tunnelUrl) ?? null,\n [agents]\n );\n const agentId = activeAgent?.id ?? '';\n const hasAgent = !!agentId;\n // Canonical agent → profile → plugins/data hierarchy. Every aiApi call\n // takes an AgentRef = { id, profile } so the path can be constructed\n // explicitly at the callsite.\n const { active: activeProfile } = useActiveAgentProfile(activeAgent?.id ?? null);\n const agentRef = useMemo(\n () => ({ id: agentId, profile: activeProfile }),\n [agentId, activeProfile]\n );\n\n const [activeTab, setActiveTab] = useQueryBackedTab<SessionTab>(SESSION_TABS, 'chat');\n\n // ── Chat state ──────────────────────────────────────────────────────\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [promptInput, setPromptInput] = useState('');\n const [sending, setSending] = useState(false);\n\n const handleSendPrompt = async () => {\n if (!promptInput.trim() || !sessionId || !hasAgent) return;\n const userMessage: ChatMessage = {\n role: 'user',\n content: promptInput,\n timestamp: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, userMessage]);\n setPromptInput('');\n setSending(true);\n try {\n const result = await sendPromptToSession(agentRef, sessionId, userMessage.content);\n const content =\n ((result as Record<string, unknown>).content as string) ||\n ((result as Record<string, unknown>).response as string) ||\n JSON.stringify(result);\n const inputTokens = (result as Record<string, unknown>).inputTokens as number | undefined;\n const outputTokens = (result as Record<string, unknown>).outputTokens as number | undefined;\n const assistantMessage: ChatMessage = {\n role: 'assistant',\n content,\n tokens:\n inputTokens !== undefined ? { input: inputTokens, output: outputTokens ?? 0 } : undefined,\n timestamp: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, assistantMessage]);\n } catch (err) {\n const errorMessage: ChatMessage = {\n role: 'assistant',\n content: `Error: ${err instanceof Error ? err.message : 'Failed to get response'}`,\n timestamp: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, errorMessage]);\n } finally {\n setSending(false);\n }\n };\n\n // ── Logs state ──────────────────────────────────────────────────────\n const [logs, setLogs] = useState<AISessionLog[]>([]);\n const [logsLoading, setLogsLoading] = useState(false);\n const [logsError, setLogsError] = useState<string | null>(null);\n const [logTypeFilter, setLogTypeFilter] = useState('all');\n\n const loadLogs = useCallback(async () => {\n if (!sessionId || !hasAgent) return;\n setLogsLoading(true);\n setLogsError(null);\n try {\n const params: Record<string, unknown> = {};\n if (logTypeFilter !== 'all') params.type = logTypeFilter;\n const result = await fetchSessionLogs(agentRef, sessionId, params);\n setLogs(result);\n } catch (err) {\n // Pre-first-message: agent has no record of this session yet —\n // render an empty list, not an error.\n if (isSessionNotFoundError(err)) {\n setLogs([]);\n } else {\n setLogsError(err instanceof Error ? err.message : 'Failed to load logs');\n }\n } finally {\n setLogsLoading(false);\n }\n }, [agentRef, sessionId, logTypeFilter, hasAgent]);\n\n useEffect(() => {\n if (activeTab === 'logs') loadLogs();\n }, [activeTab, loadLogs]);\n\n // ── Stats state ─────────────────────────────────────────────────────\n const [stats, setStats] = useState<AISessionStats | null>(null);\n const [statsLoading, setStatsLoading] = useState(false);\n const [statsError, setStatsError] = useState<string | null>(null);\n\n const loadStats = useCallback(async () => {\n if (!sessionId || !hasAgent) return;\n setStatsLoading(true);\n setStatsError(null);\n try {\n const result = await fetchSessionStats(agentRef, sessionId);\n setStats(result);\n } catch (err) {\n if (isSessionNotFoundError(err)) {\n setStats(null);\n } else {\n setStatsError(err instanceof Error ? err.message : 'Failed to load stats');\n }\n } finally {\n setStatsLoading(false);\n }\n }, [agentRef, sessionId, hasAgent]);\n\n useEffect(() => {\n if (activeTab === 'stats') loadStats();\n }, [activeTab, loadStats]);\n\n // ── Config state ────────────────────────────────────────────────────\n const [configJson, setConfigJson] = useState('{}');\n const [savingConfig, setSavingConfig] = useState(false);\n const [configSaved, setConfigSaved] = useState(false);\n const [terminating, setTerminating] = useState(false);\n\n const handleSaveConfig = async () => {\n if (!sessionId || !hasAgent) return;\n setSavingConfig(true);\n try {\n const config = JSON.parse(configJson);\n await updateSessionConfig(agentRef, sessionId, config);\n setConfigSaved(true);\n setTimeout(() => setConfigSaved(false), 3000);\n } catch (err) {\n console.error('Failed to save config:', err);\n } finally {\n setSavingConfig(false);\n }\n };\n\n const handleTerminate = async () => {\n if (!sessionId || !hasAgent) return;\n if (!window.confirm('Are you sure you want to terminate this session? This cannot be undone.'))\n return;\n setTerminating(true);\n try {\n await terminateSession(agentRef, sessionId);\n handleNavigate('/ai/sessions');\n } catch (err) {\n console.error('Failed to terminate session:', err);\n } finally {\n setTerminating(false);\n }\n };\n\n if (permissionsLoading) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <LoadingState message={tr('common.loadingPermissions', 'Loading permissions…')} />\n </MotionPage>\n );\n }\n\n if (!canReadAgents) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <AccessDenied\n title={tr('vibecontrols.pages.ai.denied.title', 'Access Denied')}\n description={tr(\n 'vibecontrols.pages.ai.denied.description',\n 'You do not have permission to view AI features.'\n )}\n />\n </MotionPage>\n );\n }\n\n if (agentsLoading) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <LoadingState message=\"Loading...\" />\n </MotionPage>\n );\n }\n\n if (!hasAgent) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <EmptyState\n icon={<MessageSquare className=\"size-8 text-text-secondary\" />}\n title={tr('vibecontrols.pages.ai.noActiveAgent', 'No active agent')}\n description={tr(\n 'vibecontrols.pages.ai.noActiveAgentDescription',\n 'An active agent with a tunnel is required to view session details.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => handleNavigate('/agents')}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors text-sm\"\n >\n {tr('vibecontrols.pages.ai.goToAgents', 'Go to Agents')}\n </button>\n }\n />\n </MotionPage>\n );\n }\n\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <PageHeader\n title={`Session ${sessionId?.substring(0, 8) ?? ''}...`}\n description={tr(\n 'vibecontrols.pages.ai.sessionDescription',\n 'AI session details and interaction'\n )}\n breadcrumb={[\n { label: 'AI', path: '#' },\n { label: 'Sessions', path: '#' },\n { label: sessionId?.substring(0, 8) ?? '' },\n ]}\n actions={\n <button\n type=\"button\"\n onClick={() => handleNavigate('/ai/sessions')}\n className=\"flex items-center gap-2 px-3 py-2 text-text-secondary hover:bg-bg-sunken rounded-md transition-colors text-sm\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibecontrols.pages.ai.back', 'Back')}\n </button>\n }\n />\n\n {/* Tab Navigation */}\n <div className=\"-mx-4 border-b border-border-default px-4 sm:-mx-6 sm:px-6\">\n <nav className=\"flex gap-1 overflow-x-auto pb-1\" role=\"tablist\">\n {sessionTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = activeTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setActiveTab(tab.id)}\n className={`flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-t-md border-b-2 px-2.5 py-2 text-xs font-medium transition-colors sm:px-3 sm:py-2.5 sm:text-sm ${\n isActive\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border-default'\n }`}\n >\n <Icon className=\"size-3.5 sm:h-4 sm:w-4\" />\n {tab.label}\n </button>\n );\n })}\n </nav>\n </div>\n\n {/* Tab Content */}\n <RequireAgentReachable agentId={agentId || null}>\n {activeTab === 'chat' && (\n <div className=\"space-y-4\">\n {/* Messages */}\n <Card className=\"p-0\">\n <div className=\"divide-y divide-border-default min-h-[200px] max-h-[50vh] overflow-y-auto\">\n {messages.length === 0 ? (\n <div className=\"flex items-center justify-center h-48 text-sm text-text-secondary\">\n {tr(\n 'vibecontrols.pages.ai.sendPromptToStart',\n 'Send a prompt to start the conversation'\n )}\n </div>\n ) : (\n messages.map((msg, idx) => (\n <div\n key={idx}\n className={`px-4 py-3 ${msg.role === 'assistant' ? 'bg-bg-sunken/50' : ''}`}\n >\n <div className=\"mb-1 flex flex-wrap items-center gap-2\">\n <span className=\"text-xs font-semibold text-text-primary uppercase\">\n {msg.role}\n </span>\n <span className=\"text-xs text-text-muted\">\n {new Date(msg.timestamp).toLocaleTimeString()}\n </span>\n {msg.tokens && (\n <span className=\"text-xs text-text-muted sm:ml-auto\">\n {msg.tokens.input + msg.tokens.output} tokens\n </span>\n )}\n </div>\n <div className=\"text-sm text-text-primary whitespace-pre-wrap\">\n {msg.content}\n </div>\n </div>\n ))\n )}\n {sending && (\n <div className=\"px-4 py-3 bg-bg-sunken/50\">\n <div className=\"flex items-center gap-2\">\n <RefreshCw className=\"size-3.5 text-text-secondary animate-spin\" />\n <span className=\"text-sm text-text-secondary\">\n {tr('vibecontrols.pages.ai.thinking', 'Thinking...')}\n </span>\n </div>\n </div>\n )}\n </div>\n </Card>\n\n {/* Prompt Input */}\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <textarea\n aria-label=\"Type your prompt...\"\n value={promptInput}\n onChange={(e) => setPromptInput(e.target.value)}\n rows={3}\n className=\"min-h-[7rem] flex-1 rounded-md border border-border-default bg-bg-sunken px-3 py-2 text-sm font-mono text-text-primary resize-none focus:outline-none focus:border-border-strong\"\n placeholder=\"Type your prompt...\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n handleSendPrompt();\n }\n }}\n />\n <button\n type=\"button\"\n onClick={handleSendPrompt}\n disabled={sending || !promptInput.trim()}\n className=\"flex items-center justify-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text transition-colors hover:bg-action-primary-bg-hover disabled:opacity-50 sm:self-end\"\n >\n <Send className=\"size-4\" />\n {tr('vibecontrols.pages.ai.send', 'Send')}\n </button>\n </div>\n </div>\n )}\n\n {activeTab === 'logs' && (\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <select\n value={logTypeFilter}\n onChange={(e) => setLogTypeFilter(e.target.value)}\n className=\"px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary text-sm\"\n >\n <option value=\"all\">All Types</option>\n <option value=\"info\">Info</option>\n <option value=\"warn\">Warning</option>\n <option value=\"error\">Error</option>\n <option value=\"debug\">Debug</option>\n <option value=\"prompt\">Prompt</option>\n <option value=\"response\">Response</option>\n </select>\n <button\n type=\"button\"\n onClick={loadLogs}\n disabled={logsLoading}\n className=\"p-2 hover:bg-bg-sunken rounded-md transition-colors\"\n >\n <RefreshCw\n className={`size-4 text-text-secondary ${logsLoading ? 'animate-spin' : ''}`}\n />\n </button>\n </div>\n\n {logsLoading ? (\n <LoadingState message=\"Loading logs...\" />\n ) : logsError ? (\n <ErrorState title=\"Failed to load logs\" message={logsError} onRetry={loadLogs} />\n ) : logs.length === 0 ? (\n <EmptyState\n icon={<ScrollText className=\"size-6 text-text-secondary\" />}\n title=\"No logs\"\n description=\"Session logs will appear here\"\n />\n ) : (\n <Card className=\"p-0\">\n <div className=\"divide-y divide-border-default max-h-[60vh] overflow-y-auto\">\n {logs.map((log) => (\n <div key={log.id} className=\"px-4 py-2 flex items-start gap-3 text-sm\">\n <span\n className={`px-1.5 py-0.5 text-xs rounded font-medium flex-shrink-0 mt-0.5 ${logTypeColors[log.type] ?? 'bg-bg-sunken text-text-secondary'}`}\n >\n {log.type}\n </span>\n <div className=\"flex-1 min-w-0\">\n <span className=\"text-text-primary\">{log.message}</span>\n </div>\n <span className=\"text-xs text-text-muted flex-shrink-0\">\n {new Date(log.timestamp).toLocaleTimeString()}\n </span>\n </div>\n ))}\n </div>\n </Card>\n )}\n </div>\n )}\n\n {activeTab === 'stats' && (\n <div className=\"space-y-4\">\n {statsLoading ? (\n <LoadingState message=\"Loading stats...\" />\n ) : statsError ? (\n <ErrorState title=\"Failed to load stats\" message={statsError} onRetry={loadStats} />\n ) : stats ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <StatCard title=\"Total Tokens\" value={stats.totalTokens.toLocaleString()} />\n <StatCard\n title=\"Input Tokens\"\n value={stats.inputTokens.toLocaleString()}\n variant=\"success\"\n />\n <StatCard\n title=\"Output Tokens\"\n value={stats.outputTokens.toLocaleString()}\n variant=\"warning\"\n />\n <StatCard title=\"Prompts\" value={stats.promptCount} />\n {stats.averageLatency !== undefined && (\n <StatCard title=\"Avg Latency\" value={`${stats.averageLatency.toFixed(0)}ms`} />\n )}\n </div>\n ) : (\n <EmptyState\n icon={<BarChart3 className=\"size-6 text-text-secondary\" />}\n title=\"No stats\"\n description=\"Stats will appear after the session processes prompts\"\n />\n )}\n </div>\n )}\n\n {activeTab === 'config' && (\n <div className=\"space-y-4\">\n <Card className=\"p-4\">\n <h3 className=\"font-semibold text-text-primary mb-3\">\n {tr('vibecontrols.pages.ai.sessionConfig', 'Session Configuration')}\n </h3>\n <textarea\n aria-label=\"control\"\n value={configJson}\n onChange={(e) => setConfigJson(e.target.value)}\n rows={10}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary text-sm font-mono focus:outline-none focus:border-border-strong\"\n placeholder='{\"model\": \"claude-3\", \"temperature\": 0.7, ...}'\n />\n <div className=\"mt-4 flex flex-col gap-3 sm:flex-row sm:items-center\">\n <button\n type=\"button\"\n onClick={handleSaveConfig}\n disabled={savingConfig}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 text-sm\"\n >\n <Settings className=\"size-4\" />\n {savingConfig\n ? tr('vibecontrols.pages.ai.saving', 'Saving...')\n : tr('vibecontrols.pages.ai.saveConfig', 'Save Config')}\n </button>\n {configSaved && (\n <span className=\"text-sm text-status-success-text\">\n {tr('vibecontrols.pages.ai.savedSuccessfully', 'Saved successfully')}\n </span>\n )}\n </div>\n </Card>\n\n <Card className=\"p-4 border-status-error-text/20\">\n <h3 className=\"font-semibold text-status-error-text mb-2\">\n {tr('vibecontrols.pages.ai.dangerZone', 'Danger Zone')}\n </h3>\n <p className=\"text-sm text-text-secondary mb-3\">\n {tr(\n 'vibecontrols.pages.ai.terminateWarning',\n 'Terminating a session is permanent. All state will be lost.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={handleTerminate}\n disabled={terminating}\n className=\"flex items-center gap-2 px-4 py-2 bg-status-error-bg text-status-error-text border border-status-error-text/20 rounded-md hover:bg-status-error-bg/80 transition-colors disabled:opacity-50 text-sm\"\n >\n <XCircle className=\"size-4\" />\n {terminating\n ? tr('vibecontrols.pages.ai.terminating', 'Terminating...')\n : tr('vibecontrols.pages.ai.terminateSession', 'Terminate Session')}\n </button>\n </Card>\n </div>\n )}\n </RequireAgentReachable>\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AISessionDetailPage.js","names":[],"sources":["../../../src/pages/ai/AISessionDetailPage.tsx"],"sourcesContent":["import { useState, useEffect, useCallback, useMemo } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n MessageSquare,\n ScrollText,\n BarChart3,\n Settings,\n Send,\n ArrowLeft,\n XCircle,\n RefreshCw,\n} from 'lucide-react';\nimport { MotionPage } from '@burdenoff/fe-libs/shared/motion';\nimport {\n PageHeader,\n Card,\n LoadingState,\n ErrorState,\n EmptyState,\n StatCard,\n RequireAgentReachable,\n} from '@/components/shared';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport { useQueryBackedTab } from '@/hooks/useQueryBackedTab';\nimport { useCachedInitialData } from '@/hooks/useCachedInitialData';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport type { AISessionLog, AISessionStats } from '@/services/aiApi';\nimport { AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n sendPromptToSession,\n fetchSessionLogs,\n fetchSessionStats,\n updateSessionConfig,\n terminateSession,\n} from '@/services/aiApi';\nimport { isSessionNotFoundError } from '@/utils/aiSessionNotFound';\n\ntype SessionTab = 'chat' | 'logs' | 'stats' | 'config';\nconst SESSION_TABS: readonly SessionTab[] = ['chat', 'logs', 'stats', 'config'];\n\nconst sessionTabs: { id: SessionTab; label: string; icon: typeof MessageSquare }[] = [\n { id: 'chat', label: 'Chat', icon: MessageSquare },\n { id: 'logs', label: 'Logs', icon: ScrollText },\n { id: 'stats', label: 'Stats', icon: BarChart3 },\n { id: 'config', label: 'Config', icon: Settings },\n];\n\nconst logTypeColors: Record<string, string> = {\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n warn: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n debug: 'bg-bg-sunken text-text-secondary',\n prompt: 'bg-status-success-bg-subtle text-status-success-text',\n response: 'bg-[var(--color-accent-soft)] text-text-primary',\n};\n\ninterface ChatMessage {\n role: 'user' | 'assistant';\n content: string;\n tokens?: { input: number; output: number };\n timestamp: string;\n}\n\n/**\n * AI session detail page with Chat, Logs, Stats, and Config tabs.\n */\nexport function AISessionDetailPage() {\n const tr = useTr();\n // Agent-centric page — gate on `agent:read` (defense-in-depth).\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const canReadAgents = hasPermission(VIBE_PERMISSIONS.AGENT_READ);\n const { sessionId } = useParams<{ sessionId: string }>();\n const handleNavigate = useVibeNavigate();\n\n const { agents, loading: agentsLoading } = useCachedInitialData();\n const activeAgent = useMemo(\n () => agents.find((agent) => agent.isActive && agent.tunnelUrl) ?? null,\n [agents]\n );\n const agentId = activeAgent?.id ?? '';\n const hasAgent = !!agentId;\n // Canonical agent → profile → plugins/data hierarchy. Every aiApi call\n // takes an AgentRef = { id, profile } so the path can be constructed\n // explicitly at the callsite.\n const { active: activeProfile } = useActiveAgentProfile(activeAgent?.id ?? null);\n const agentRef = useMemo(\n () => ({ id: agentId, profile: activeProfile }),\n [agentId, activeProfile]\n );\n\n const [activeTab, setActiveTab] = useQueryBackedTab<SessionTab>(SESSION_TABS, 'chat');\n\n // ── Chat state ──────────────────────────────────────────────────────\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [promptInput, setPromptInput] = useState('');\n const [sending, setSending] = useState(false);\n\n const handleSendPrompt = async () => {\n if (!promptInput.trim() || !sessionId || !hasAgent) return;\n const userMessage: ChatMessage = {\n role: 'user',\n content: promptInput,\n timestamp: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, userMessage]);\n setPromptInput('');\n setSending(true);\n try {\n const result = await sendPromptToSession(agentRef, sessionId, userMessage.content);\n const content =\n ((result as Record<string, unknown>).content as string) ||\n ((result as Record<string, unknown>).response as string) ||\n JSON.stringify(result);\n const inputTokens = (result as Record<string, unknown>).inputTokens as number | undefined;\n const outputTokens = (result as Record<string, unknown>).outputTokens as number | undefined;\n const assistantMessage: ChatMessage = {\n role: 'assistant',\n content,\n tokens:\n inputTokens !== undefined ? { input: inputTokens, output: outputTokens ?? 0 } : undefined,\n timestamp: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, assistantMessage]);\n } catch (err) {\n const errorMessage: ChatMessage = {\n role: 'assistant',\n content: `Error: ${err instanceof Error ? err.message : 'Failed to get response'}`,\n timestamp: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, errorMessage]);\n } finally {\n setSending(false);\n }\n };\n\n // ── Logs state ──────────────────────────────────────────────────────\n const [logs, setLogs] = useState<AISessionLog[]>([]);\n const [logsLoading, setLogsLoading] = useState(false);\n const [logsError, setLogsError] = useState<string | null>(null);\n const [logTypeFilter, setLogTypeFilter] = useState('all');\n\n const loadLogs = useCallback(async () => {\n if (!sessionId || !hasAgent) return;\n setLogsLoading(true);\n setLogsError(null);\n try {\n const params: Record<string, unknown> = {};\n if (logTypeFilter !== 'all') params.type = logTypeFilter;\n const result = await fetchSessionLogs(agentRef, sessionId, params);\n setLogs(result);\n } catch (err) {\n // Pre-first-message: agent has no record of this session yet —\n // render an empty list, not an error.\n if (isSessionNotFoundError(err)) {\n setLogs([]);\n } else {\n setLogsError(err instanceof Error ? err.message : 'Failed to load logs');\n }\n } finally {\n setLogsLoading(false);\n }\n }, [agentRef, sessionId, logTypeFilter, hasAgent]);\n\n useEffect(() => {\n if (activeTab === 'logs') loadLogs();\n }, [activeTab, loadLogs]);\n\n // ── Stats state ─────────────────────────────────────────────────────\n const [stats, setStats] = useState<AISessionStats | null>(null);\n const [statsLoading, setStatsLoading] = useState(false);\n const [statsError, setStatsError] = useState<string | null>(null);\n\n const loadStats = useCallback(async () => {\n if (!sessionId || !hasAgent) return;\n setStatsLoading(true);\n setStatsError(null);\n try {\n const result = await fetchSessionStats(agentRef, sessionId);\n setStats(result);\n } catch (err) {\n if (isSessionNotFoundError(err)) {\n setStats(null);\n } else {\n setStatsError(err instanceof Error ? err.message : 'Failed to load stats');\n }\n } finally {\n setStatsLoading(false);\n }\n }, [agentRef, sessionId, hasAgent]);\n\n useEffect(() => {\n if (activeTab === 'stats') loadStats();\n }, [activeTab, loadStats]);\n\n // ── Config state ────────────────────────────────────────────────────\n const [configJson, setConfigJson] = useState('{}');\n const [savingConfig, setSavingConfig] = useState(false);\n const [configSaved, setConfigSaved] = useState(false);\n const [terminating, setTerminating] = useState(false);\n\n const handleSaveConfig = async () => {\n if (!sessionId || !hasAgent) return;\n setSavingConfig(true);\n try {\n const config = JSON.parse(configJson);\n await updateSessionConfig(agentRef, sessionId, config);\n setConfigSaved(true);\n setTimeout(() => setConfigSaved(false), 3000);\n } catch (err) {\n console.error('Failed to save config:', err);\n } finally {\n setSavingConfig(false);\n }\n };\n\n const handleTerminate = async () => {\n if (!sessionId || !hasAgent) return;\n if (!window.confirm('Are you sure you want to terminate this session? This cannot be undone.'))\n return;\n setTerminating(true);\n try {\n await terminateSession(agentRef, sessionId);\n handleNavigate('/ai/sessions');\n } catch (err) {\n console.error('Failed to terminate session:', err);\n } finally {\n setTerminating(false);\n }\n };\n\n if (permissionsLoading) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <LoadingState message={tr('common.loadingPermissions', 'Loading permissions…')} />\n </MotionPage>\n );\n }\n\n if (!canReadAgents) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <AccessDenied\n title={tr('vibecontrols.pages.ai.denied.title', 'Access Denied')}\n description={tr(\n 'vibecontrols.pages.ai.denied.description',\n 'You do not have permission to view AI features.'\n )}\n />\n </MotionPage>\n );\n }\n\n if (agentsLoading) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <LoadingState message=\"Loading...\" />\n </MotionPage>\n );\n }\n\n if (!hasAgent) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <EmptyState\n icon={<MessageSquare className=\"size-8 text-text-secondary\" />}\n title={tr('vibecontrols.pages.ai.noActiveAgent', 'No active agent')}\n description={tr(\n 'vibecontrols.pages.ai.noActiveAgentDescription',\n 'An active agent with a tunnel is required to view session details.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => handleNavigate('/agents')}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors text-sm\"\n >\n {tr('vibecontrols.pages.ai.goToAgents', 'Go to Agents')}\n </button>\n }\n />\n </MotionPage>\n );\n }\n\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <PageHeader\n title={`Session ${sessionId?.substring(0, 8) ?? ''}...`}\n description={tr(\n 'vibecontrols.pages.ai.sessionDescription',\n 'AI session details and interaction'\n )}\n breadcrumb={[\n { label: 'AI', path: '#' },\n { label: 'Sessions', path: '#' },\n { label: sessionId?.substring(0, 8) ?? '' },\n ]}\n actions={\n <button\n type=\"button\"\n onClick={() => handleNavigate('/ai/sessions')}\n className=\"flex items-center gap-2 px-3 py-2 text-text-secondary hover:bg-bg-sunken rounded-md transition-colors text-sm\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibecontrols.pages.ai.back', 'Back')}\n </button>\n }\n />\n\n {/* Tab Navigation */}\n <div className=\"-mx-4 border-b border-border-default px-4 sm:-mx-6 sm:px-6\">\n <nav className=\"flex gap-1 overflow-x-auto pb-1\" role=\"tablist\">\n {sessionTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = activeTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setActiveTab(tab.id)}\n className={`flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-t-md border-b-2 px-2.5 py-2 text-xs font-medium transition-colors sm:px-3 sm:py-2.5 sm:text-sm ${\n isActive\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border-default'\n }`}\n >\n <Icon className=\"size-3.5 sm:h-4 sm:w-4\" />\n {tab.label}\n </button>\n );\n })}\n </nav>\n </div>\n\n {/* Tab Content */}\n <RequireAgentReachable agentId={agentId || null}>\n {activeTab === 'chat' && (\n <div className=\"space-y-4\">\n {/* Messages */}\n <Card className=\"p-0\">\n <div className=\"divide-y divide-border-default min-h-[200px] max-h-[50vh] overflow-y-auto\">\n {messages.length === 0 ? (\n <div className=\"flex items-center justify-center h-48 text-sm text-text-secondary\">\n {tr(\n 'vibecontrols.pages.ai.sendPromptToStart',\n 'Send a prompt to start the conversation'\n )}\n </div>\n ) : (\n messages.map((msg, idx) => (\n <div\n key={idx}\n className={`px-4 py-3 ${msg.role === 'assistant' ? 'bg-bg-sunken/50' : ''}`}\n >\n <div className=\"mb-1 flex flex-wrap items-center gap-2\">\n <span className=\"text-xs font-semibold text-text-primary uppercase\">\n {msg.role}\n </span>\n <span className=\"text-xs text-text-muted\">\n {new Date(msg.timestamp).toLocaleTimeString()}\n </span>\n {msg.tokens && (\n <span className=\"text-xs text-text-muted sm:ml-auto\">\n {msg.tokens.input + msg.tokens.output} tokens\n </span>\n )}\n </div>\n <div className=\"text-sm text-text-primary whitespace-pre-wrap\">\n {msg.content}\n </div>\n </div>\n ))\n )}\n {sending && (\n <div className=\"px-4 py-3 bg-bg-sunken/50\">\n <div className=\"flex items-center gap-2\">\n <RefreshCw className=\"size-3.5 text-text-secondary animate-spin\" />\n <span className=\"text-sm text-text-secondary\">\n {tr('vibecontrols.pages.ai.thinking', 'Thinking...')}\n </span>\n </div>\n </div>\n )}\n </div>\n </Card>\n\n {/* Prompt Input */}\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <textarea\n aria-label=\"Type your prompt...\"\n value={promptInput}\n onChange={(e) => setPromptInput(e.target.value)}\n rows={3}\n className=\"min-h-[7rem] flex-1 rounded-md border border-border-default bg-bg-sunken px-3 py-2 text-sm font-mono text-text-primary resize-none focus:outline-none focus:border-border-strong\"\n placeholder=\"Type your prompt...\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n handleSendPrompt();\n }\n }}\n />\n <button\n type=\"button\"\n onClick={handleSendPrompt}\n disabled={sending || !promptInput.trim()}\n className=\"flex items-center justify-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text transition-colors hover:bg-action-primary-bg-hover disabled:opacity-50 sm:self-end\"\n >\n <Send className=\"size-4\" />\n {tr('vibecontrols.pages.ai.send', 'Send')}\n </button>\n </div>\n </div>\n )}\n\n {activeTab === 'logs' && (\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <select\n value={logTypeFilter}\n onChange={(e) => setLogTypeFilter(e.target.value)}\n className=\"px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary text-sm\"\n >\n <option value=\"all\">All Types</option>\n <option value=\"info\">Info</option>\n <option value=\"warn\">Warning</option>\n <option value=\"error\">Error</option>\n <option value=\"debug\">Debug</option>\n <option value=\"prompt\">Prompt</option>\n <option value=\"response\">Response</option>\n </select>\n <button\n type=\"button\"\n onClick={loadLogs}\n disabled={logsLoading}\n className=\"p-2 hover:bg-bg-sunken rounded-md transition-colors\"\n >\n <RefreshCw\n className={`size-4 text-text-secondary ${logsLoading ? 'animate-spin' : ''}`}\n />\n </button>\n </div>\n\n {logsLoading ? (\n <LoadingState message=\"Loading logs...\" />\n ) : logsError ? (\n <ErrorState title=\"Failed to load logs\" message={logsError} onRetry={loadLogs} />\n ) : logs.length === 0 ? (\n <EmptyState\n icon={<ScrollText className=\"size-6 text-text-secondary\" />}\n title=\"No logs\"\n description=\"Session logs will appear here\"\n />\n ) : (\n <Card className=\"p-0\">\n <div className=\"divide-y divide-border-default max-h-[60vh] overflow-y-auto\">\n {logs.map((log) => (\n <div key={log.id} className=\"px-4 py-2 flex items-start gap-3 text-sm\">\n <span\n className={`px-1.5 py-0.5 text-xs rounded font-medium flex-shrink-0 mt-0.5 ${logTypeColors[log.type] ?? 'bg-bg-sunken text-text-secondary'}`}\n >\n {log.type}\n </span>\n <div className=\"flex-1 min-w-0\">\n <span className=\"text-text-primary\">{log.message}</span>\n </div>\n <span className=\"text-xs text-text-muted flex-shrink-0\">\n {new Date(log.timestamp).toLocaleTimeString()}\n </span>\n </div>\n ))}\n </div>\n </Card>\n )}\n </div>\n )}\n\n {activeTab === 'stats' && (\n <div className=\"space-y-4\">\n {statsLoading ? (\n <LoadingState message=\"Loading stats...\" />\n ) : statsError ? (\n <ErrorState title=\"Failed to load stats\" message={statsError} onRetry={loadStats} />\n ) : stats ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <StatCard title=\"Total Tokens\" value={stats.totalTokens.toLocaleString()} />\n <StatCard\n title=\"Input Tokens\"\n value={stats.inputTokens.toLocaleString()}\n variant=\"success\"\n />\n <StatCard\n title=\"Output Tokens\"\n value={stats.outputTokens.toLocaleString()}\n variant=\"warning\"\n />\n <StatCard title=\"Prompts\" value={stats.promptCount} />\n {stats.averageLatency !== undefined && (\n <StatCard title=\"Avg Latency\" value={`${stats.averageLatency.toFixed(0)}ms`} />\n )}\n </div>\n ) : (\n <EmptyState\n icon={<BarChart3 className=\"size-6 text-text-secondary\" />}\n title=\"No stats\"\n description=\"Stats will appear after the session processes prompts\"\n />\n )}\n </div>\n )}\n\n {activeTab === 'config' && (\n <div className=\"space-y-4\">\n <Card className=\"p-4\">\n <h3 className=\"font-semibold text-text-primary mb-3\">\n {tr('vibecontrols.pages.ai.sessionConfig', 'Session Configuration')}\n </h3>\n <textarea\n aria-label=\"control\"\n value={configJson}\n onChange={(e) => setConfigJson(e.target.value)}\n rows={10}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary text-sm font-mono focus:outline-none focus:border-border-strong\"\n placeholder='{\"model\": \"claude-3\", \"temperature\": 0.7, ...}'\n />\n <div className=\"mt-4 flex flex-col gap-3 sm:flex-row sm:items-center\">\n <button\n type=\"button\"\n onClick={handleSaveConfig}\n disabled={savingConfig}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 text-sm\"\n >\n <Settings className=\"size-4\" />\n {savingConfig\n ? tr('vibecontrols.pages.ai.saving', 'Saving...')\n : tr('vibecontrols.pages.ai.saveConfig', 'Save Config')}\n </button>\n {configSaved && (\n <span className=\"text-sm text-status-success-text\">\n {tr('vibecontrols.pages.ai.savedSuccessfully', 'Saved successfully')}\n </span>\n )}\n </div>\n </Card>\n\n <Card className=\"p-4 border-status-error-text/20\">\n <h3 className=\"font-semibold text-status-error-text mb-2\">\n {tr('vibecontrols.pages.ai.dangerZone', 'Danger Zone')}\n </h3>\n <p className=\"text-sm text-text-secondary mb-3\">\n {tr(\n 'vibecontrols.pages.ai.terminateWarning',\n 'Terminating a session is permanent. All state will be lost.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={handleTerminate}\n disabled={terminating}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-danger-bg text-action-danger-text border border-status-error-border rounded-button hover:bg-action-danger-bg-hover transition-colors disabled:opacity-50 text-sm\"\n >\n <XCircle className=\"size-4\" />\n {terminating\n ? tr('vibecontrols.pages.ai.terminating', 'Terminating...')\n : tr('vibecontrols.pages.ai.terminateSession', 'Terminate Session')}\n </button>\n </Card>\n </div>\n )}\n </RequireAgentReachable>\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
|
@@ -84,7 +84,7 @@ function d() {
|
|
|
84
84
|
/* @__PURE__ */ c("td", {
|
|
85
85
|
className: "px-4 py-3",
|
|
86
86
|
children: /* @__PURE__ */ c("span", {
|
|
87
|
-
className: `inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${e.status === "SUCCESS" ? "bg-status-success-bg text-status-success-
|
|
87
|
+
className: `inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${e.status === "SUCCESS" ? "bg-status-success-bg-subtle text-status-success-text" : "bg-status-error-bg-subtle text-status-error-text"}`,
|
|
88
88
|
children: e.status
|
|
89
89
|
})
|
|
90
90
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuditTab.js","names":[],"sources":["../../../src/pages/tunnels/AuditTab.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport {\n useWorkspaceTunnelAuditQuery,\n type WorkspaceTunnelAuditQuery,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype AuditEntry = WorkspaceTunnelAuditQuery['workspaceTunnelAudit']['items'][number];\n\nconst PAGE_SIZE = 50;\n\nexport function AuditTab() {\n const tr = useTr();\n const [offset, setOffset] = useState(0);\n\n const { data, loading, error, refetch } = useWorkspaceTunnelAuditQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n fetchPolicy: 'cache-and-network',\n });\n\n const items = data?.workspaceTunnelAudit?.items ?? [];\n const total = data?.workspaceTunnelAudit?.total ?? 0;\n const hasMore = data?.workspaceTunnelAudit?.hasMore ?? false;\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.audit.title', 'Audit')}\n description={tr(\n 'vibecontrols.tunnels.audit.description',\n 'Operations recorded across every tunnel in the workspace.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading audit log\" />}\n {error && (\n <ErrorState\n title=\"Failed to load audit log\"\n message={error.message}\n onRetry={() => void refetch()}\n />\n )}\n\n {data && items.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.audit.emptyTitle', 'No audit entries yet')}\n description={tr(\n 'vibecontrols.tunnels.audit.emptyDescription',\n 'Audit entries are recorded for every create, start, stop, rotate, attach, detach, verify, and delete operation.'\n )}\n />\n )}\n\n {items.length > 0 && (\n <>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n <table className=\"w-full text-sm min-w-[560px]\">\n <thead className=\"border-b border-border-subtle bg-bg-sunken text-text-secondary\">\n <tr>\n <th className=\"px-4 py-3 text-left font-medium\">Action</th>\n <th className=\"px-4 py-3 text-left font-medium\">Resource</th>\n <th className=\"px-4 py-3 text-left font-medium\">Status</th>\n <th className=\"px-4 py-3 text-left font-medium\">Actor</th>\n <th className=\"px-4 py-3 text-left font-medium whitespace-nowrap\">When</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {items.map((entry: AuditEntry) => (\n <tr key={entry.id} className=\"hover:bg-bg-sunken\">\n <td className=\"px-4 py-3 text-text-primary whitespace-nowrap\">\n {entry.operationName ?? entry.action}\n </td>\n <td className=\"px-4 py-3 text-text-secondary\">\n <div className=\"whitespace-nowrap\">{entry.resourceType}</div>\n {entry.resourceName && (\n <div\n className=\"text-xs text-text-muted truncate max-w-[180px]\"\n title={entry.resourceName}\n >\n {entry.resourceName}\n </div>\n )}\n </td>\n <td className=\"px-4 py-3\">\n <span\n className={`inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${\n entry.status === 'SUCCESS'\n ? 'bg-status-success-bg text-status-success-
|
|
1
|
+
{"version":3,"file":"AuditTab.js","names":[],"sources":["../../../src/pages/tunnels/AuditTab.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport {\n useWorkspaceTunnelAuditQuery,\n type WorkspaceTunnelAuditQuery,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype AuditEntry = WorkspaceTunnelAuditQuery['workspaceTunnelAudit']['items'][number];\n\nconst PAGE_SIZE = 50;\n\nexport function AuditTab() {\n const tr = useTr();\n const [offset, setOffset] = useState(0);\n\n const { data, loading, error, refetch } = useWorkspaceTunnelAuditQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n fetchPolicy: 'cache-and-network',\n });\n\n const items = data?.workspaceTunnelAudit?.items ?? [];\n const total = data?.workspaceTunnelAudit?.total ?? 0;\n const hasMore = data?.workspaceTunnelAudit?.hasMore ?? false;\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.audit.title', 'Audit')}\n description={tr(\n 'vibecontrols.tunnels.audit.description',\n 'Operations recorded across every tunnel in the workspace.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading audit log\" />}\n {error && (\n <ErrorState\n title=\"Failed to load audit log\"\n message={error.message}\n onRetry={() => void refetch()}\n />\n )}\n\n {data && items.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.audit.emptyTitle', 'No audit entries yet')}\n description={tr(\n 'vibecontrols.tunnels.audit.emptyDescription',\n 'Audit entries are recorded for every create, start, stop, rotate, attach, detach, verify, and delete operation.'\n )}\n />\n )}\n\n {items.length > 0 && (\n <>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n <table className=\"w-full text-sm min-w-[560px]\">\n <thead className=\"border-b border-border-subtle bg-bg-sunken text-text-secondary\">\n <tr>\n <th className=\"px-4 py-3 text-left font-medium\">Action</th>\n <th className=\"px-4 py-3 text-left font-medium\">Resource</th>\n <th className=\"px-4 py-3 text-left font-medium\">Status</th>\n <th className=\"px-4 py-3 text-left font-medium\">Actor</th>\n <th className=\"px-4 py-3 text-left font-medium whitespace-nowrap\">When</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {items.map((entry: AuditEntry) => (\n <tr key={entry.id} className=\"hover:bg-bg-sunken\">\n <td className=\"px-4 py-3 text-text-primary whitespace-nowrap\">\n {entry.operationName ?? entry.action}\n </td>\n <td className=\"px-4 py-3 text-text-secondary\">\n <div className=\"whitespace-nowrap\">{entry.resourceType}</div>\n {entry.resourceName && (\n <div\n className=\"text-xs text-text-muted truncate max-w-[180px]\"\n title={entry.resourceName}\n >\n {entry.resourceName}\n </div>\n )}\n </td>\n <td className=\"px-4 py-3\">\n <span\n className={`inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${\n entry.status === 'SUCCESS'\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-status-error-bg-subtle text-status-error-text'\n }`}\n >\n {entry.status}\n </span>\n </td>\n <td\n className=\"px-4 py-3 text-xs text-text-muted font-mono truncate max-w-[140px]\"\n title={entry.actorId}\n >\n {entry.actorId}\n </td>\n <td className=\"px-4 py-3 text-xs text-text-muted whitespace-nowrap\">\n {new Date(entry.createdAt).toLocaleString()}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"flex items-center justify-between mt-4 text-sm text-text-secondary\">\n <span>\n {offset + 1}–{Math.min(offset + items.length, total)} of {total}\n </span>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset(Math.max(0, offset - PAGE_SIZE))}\n className=\"rounded-md border border-border-default px-3 py-1 text-xs disabled:opacity-40 hover:bg-bg-sunken\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset(offset + PAGE_SIZE)}\n className=\"rounded-md border border-border-default px-3 py-1 text-xs disabled:opacity-40 hover:bg-bg-sunken\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -97,7 +97,7 @@ function m() {
|
|
|
97
97
|
}), /* @__PURE__ */ f("button", {
|
|
98
98
|
type: "button",
|
|
99
99
|
onClick: () => void S({ variables: { domainId: e.id } }),
|
|
100
|
-
className: "rounded-md border border-border-default px-3 py-1 text-xs text-status-error-
|
|
100
|
+
className: "rounded-md border border-border-default px-3 py-1 text-xs text-status-error-text hover:bg-status-error-bg-subtle",
|
|
101
101
|
children: "Delete"
|
|
102
102
|
})]
|
|
103
103
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DomainsTab.js","names":[],"sources":["../../../src/pages/tunnels/DomainsTab.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { DomainVerificationBadge } from '@/components/tunnels/DomainVerificationBadge';\nimport { TunnelDomainType, TunnelDomainVerificationStatus } from '@/generated/wspace-types';\nimport {\n useListTunnelDomainsQuery,\n useVerifyTunnelDomainMutation,\n useDeleteTunnelDomainMutation,\n} from '@/generated/wspace-operations';\n\nexport function DomainsTab() {\n const tr = useTr();\n const [verifyingId, setVerifyingId] = useState<string | null>(null);\n const { data, loading, error, refetch } = useListTunnelDomainsQuery({\n variables: { pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const [verifyDomain] = useVerifyTunnelDomainMutation({\n onCompleted: () => {\n setVerifyingId(null);\n void refetch();\n },\n });\n const [deleteDomain] = useDeleteTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n\n const domains = data?.tunnelDomains?.items ?? [];\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto overflow-x-hidden\">\n <PageHeader\n title={tr('vibecontrols.tunnels.domains.title', 'Domains')}\n description={tr(\n 'vibecontrols.tunnels.domains.description',\n 'Managed *.vibetunnels.com subdomains and custom user-owned domains.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading domains\" />}\n {error && (\n <ErrorState\n title=\"Failed to load domains\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && domains.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.domains.emptyTitle', 'No domains yet')}\n description={tr(\n 'vibecontrols.tunnels.domains.emptyDescription',\n 'Reserve a *.vibetunnels.com subdomain or register a custom domain from the Tunnels tab.'\n )}\n />\n )}\n\n {domains.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n <table className=\"min-w-[580px] w-full text-sm\">\n <thead className=\"border-b border-border-subtle bg-bg-sunken text-text-secondary\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted\">\n Hostname\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Type\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Status\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Tunnel\n </th>\n <th className=\"px-4 py-3 text-right text-xs font-medium text-text-muted whitespace-nowrap\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {domains.map((domain: any) => (\n <tr key={domain.id} className=\"hover:bg-bg-sunken\">\n <td className=\"px-4 py-3 font-mono text-sm text-text-primary break-all min-w-[160px]\">\n {domain.hostname}\n </td>\n <td className=\"px-4 py-3 text-xs text-text-secondary whitespace-nowrap\">\n {domain.type === TunnelDomainType.ManagedSubdomain ? 'Managed' : 'Custom'}\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap\">\n <DomainVerificationBadge status={domain.verificationStatus} />\n </td>\n <td className=\"px-4 py-3 text-xs text-text-muted whitespace-nowrap font-mono\">\n {domain.tunnelId ?? '—'}\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap\">\n <div className=\"flex gap-2 justify-end\">\n {domain.verificationStatus === TunnelDomainVerificationStatus.Pending && (\n <button\n type=\"button\"\n disabled={verifyingId === domain.id}\n onClick={() => {\n setVerifyingId(domain.id);\n void verifyDomain({ variables: { domainId: domain.id } });\n }}\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-text-primary hover:bg-bg-elevated disabled:opacity-50\"\n >\n {verifyingId === domain.id ? 'Checking…' : 'Verify now'}\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => void deleteDomain({ variables: { domainId: domain.id } })}\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-status-error-
|
|
1
|
+
{"version":3,"file":"DomainsTab.js","names":[],"sources":["../../../src/pages/tunnels/DomainsTab.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { DomainVerificationBadge } from '@/components/tunnels/DomainVerificationBadge';\nimport { TunnelDomainType, TunnelDomainVerificationStatus } from '@/generated/wspace-types';\nimport {\n useListTunnelDomainsQuery,\n useVerifyTunnelDomainMutation,\n useDeleteTunnelDomainMutation,\n} from '@/generated/wspace-operations';\n\nexport function DomainsTab() {\n const tr = useTr();\n const [verifyingId, setVerifyingId] = useState<string | null>(null);\n const { data, loading, error, refetch } = useListTunnelDomainsQuery({\n variables: { pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const [verifyDomain] = useVerifyTunnelDomainMutation({\n onCompleted: () => {\n setVerifyingId(null);\n void refetch();\n },\n });\n const [deleteDomain] = useDeleteTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n\n const domains = data?.tunnelDomains?.items ?? [];\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto overflow-x-hidden\">\n <PageHeader\n title={tr('vibecontrols.tunnels.domains.title', 'Domains')}\n description={tr(\n 'vibecontrols.tunnels.domains.description',\n 'Managed *.vibetunnels.com subdomains and custom user-owned domains.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading domains\" />}\n {error && (\n <ErrorState\n title=\"Failed to load domains\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && domains.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.domains.emptyTitle', 'No domains yet')}\n description={tr(\n 'vibecontrols.tunnels.domains.emptyDescription',\n 'Reserve a *.vibetunnels.com subdomain or register a custom domain from the Tunnels tab.'\n )}\n />\n )}\n\n {domains.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n <table className=\"min-w-[580px] w-full text-sm\">\n <thead className=\"border-b border-border-subtle bg-bg-sunken text-text-secondary\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted\">\n Hostname\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Type\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Status\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Tunnel\n </th>\n <th className=\"px-4 py-3 text-right text-xs font-medium text-text-muted whitespace-nowrap\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {domains.map((domain: any) => (\n <tr key={domain.id} className=\"hover:bg-bg-sunken\">\n <td className=\"px-4 py-3 font-mono text-sm text-text-primary break-all min-w-[160px]\">\n {domain.hostname}\n </td>\n <td className=\"px-4 py-3 text-xs text-text-secondary whitespace-nowrap\">\n {domain.type === TunnelDomainType.ManagedSubdomain ? 'Managed' : 'Custom'}\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap\">\n <DomainVerificationBadge status={domain.verificationStatus} />\n </td>\n <td className=\"px-4 py-3 text-xs text-text-muted whitespace-nowrap font-mono\">\n {domain.tunnelId ?? '—'}\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap\">\n <div className=\"flex gap-2 justify-end\">\n {domain.verificationStatus === TunnelDomainVerificationStatus.Pending && (\n <button\n type=\"button\"\n disabled={verifyingId === domain.id}\n onClick={() => {\n setVerifyingId(domain.id);\n void verifyDomain({ variables: { domainId: domain.id } });\n }}\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-text-primary hover:bg-bg-elevated disabled:opacity-50\"\n >\n {verifyingId === domain.id ? 'Checking…' : 'Verify now'}\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => void deleteDomain({ variables: { domainId: domain.id } })}\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-status-error-text hover:bg-status-error-bg-subtle\"\n >\n Delete\n </button>\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAcA,SAAgB,IAAa;CAC3B,IAAM,IAAK,GAAO,EACZ,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,EAAE,SAAM,YAAS,UAAO,eAAY,EAA0B;EAClE,WAAW,EAAE,YAAY,EAAE,OAAO,IAAI,EAAE;EACxC,aAAa;EACd,CAAC,EAEI,CAAC,KAAgB,EAA8B,EACnD,mBAAmB;AAEZ,EADL,EAAe,KAAK,EACf,GAAS;IAEjB,CAAC,EACI,CAAC,KAAgB,EAA8B,EACnD,mBAAmB,KAAK,GAAS,EAClC,CAAC,EAEI,IAAU,GAAM,eAAe,SAAS,EAAE;AAEhD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAO,EAAG,sCAAsC,UAAU;IAC1D,aAAa,EACX,4CACA,sEACD;IACD,CAAA;GAED,KAAW,CAAC,KAAQ,kBAAC,GAAD,EAAc,SAAQ,mBAAoB,CAAA;GAC9D,KACC,kBAAC,GAAD;IACE,OAAM;IACN,SAAS,EAAM;IACf,eAAe,GAAS;IACxB,CAAA;GAGH,KAAQ,EAAQ,WAAW,KAC1B,kBAAC,GAAD;IACE,OAAO,EAAG,2CAA2C,iBAAiB;IACtE,aAAa,EACX,iDACA,0FACD;IACD,CAAA;GAGH,EAAQ,SAAS,KAChB,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD;MAAO,WAAU;gBACf,kBAAC,MAAD,EAAA,UAAA;OACE,kBAAC,MAAD;QAAI,WAAU;kBAA0D;QAEnE,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA4E;QAErF,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA4E;QAErF,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA4E;QAErF,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA6E;QAEtF,CAAA;OACF,EAAA,CAAA;MACC,CAAA,EACR,kBAAC,SAAD;MAAO,WAAU;gBACd,EAAQ,KAAK,MACZ,kBAAC,MAAD;OAAoB,WAAU;iBAA9B;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAO;SACL,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAO,SAAS,EAAiB,mBAAmB,YAAY;SAC9D,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,GAAD,EAAyB,QAAQ,EAAO,oBAAsB,CAAA;SAC3D,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAO,YAAY;SACjB,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACG,EAAO,uBAAuB,EAA+B,WAC5D,kBAAC,UAAD;WACE,MAAK;WACL,UAAU,MAAgB,EAAO;WACjC,eAAe;AAER,YADL,EAAe,EAAO,GAAG,EACpB,EAAa,EAAE,WAAW,EAAE,UAAU,EAAO,IAAI,EAAE,CAAC;;WAE3D,WAAU;qBAET,MAAgB,EAAO,KAAK,cAAc;WACpC,CAAA,EAEX,kBAAC,UAAD;WACE,MAAK;WACL,eAAe,KAAK,EAAa,EAAE,WAAW,EAAE,UAAU,EAAO,IAAI,EAAE,CAAC;WACxE,WAAU;qBACX;WAEQ,CAAA,CACL;;SACH,CAAA;QACF;SArCI,EAAO,GAqCX,CACL;MACI,CAAA,CACF;;IACJ,CAAA;GAEJ"}
|
|
@@ -44,7 +44,7 @@ function c() {
|
|
|
44
44
|
className: "text-xs text-text-muted",
|
|
45
45
|
children: e.key
|
|
46
46
|
})] }), /* @__PURE__ */ o("span", {
|
|
47
|
-
className: `rounded-full px-2 py-0.5 text-xs ${e.isEnabled ? "bg-status-success-bg text-status-success-
|
|
47
|
+
className: `rounded-full px-2 py-0.5 text-xs ${e.isEnabled ? "bg-status-success-bg-subtle text-status-success-text" : "bg-bg-sunken text-text-muted"}`,
|
|
48
48
|
children: e.isEnabled ? "Enabled" : "Disabled"
|
|
49
49
|
})]
|
|
50
50
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProvidersTab.js","names":[],"sources":["../../../src/pages/tunnels/ProvidersTab.tsx"],"sourcesContent":["import { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { useListTunnelProvidersQuery } from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport function ProvidersTab() {\n const tr = useTr();\n const { data, loading, error, refetch } = useListTunnelProvidersQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n // Show VibeTunnels first (preferred default), then Cloudflare, then any\n // future providers alphabetically. This matches the ordering used in the\n // tunnels list filter and the create wizard's provider selector.\n const providers = [...(data?.tunnelProviders ?? [])].sort(\n (a: { key: string }, b: { key: string }) => {\n const rank = (key: string) => (key === 'VIBETUNNELS' ? 0 : key === 'CLOUDFLARE' ? 1 : 2);\n const diff = rank(a.key) - rank(b.key);\n if (diff !== 0) return diff;\n return a.key.localeCompare(b.key);\n }\n );\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.providers.title', 'Providers')}\n description={tr(\n 'vibecontrols.tunnels.providers.description',\n 'Tunnel providers registered for this workspace with their capabilities.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading providers\" />}\n {error && (\n <ErrorState\n title=\"Failed to load providers\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && providers.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.providers.emptyTitle', 'No providers registered')}\n description={tr(\n 'vibecontrols.tunnels.providers.emptyDescription',\n 'Contact your workspace admin to enable VibeTunnels or Cloudflare providers.'\n )}\n />\n )}\n\n {data && providers.length > 0 && (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {providers.map((provider: any) => {\n const caps = provider.capabilities;\n return (\n <div\n key={provider.id}\n className={`rounded-lg border p-4 ${\n provider.isEnabled\n ? 'border-border-default bg-bg-surface'\n : 'border-border-subtle bg-bg-sunken opacity-60'\n }`}\n >\n <div className=\"flex items-start justify-between gap-3 mb-3\">\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {provider.displayName}\n </h3>\n <p className=\"text-xs text-text-muted\">{provider.key}</p>\n </div>\n <span\n className={`rounded-full px-2 py-0.5 text-xs ${\n provider.isEnabled\n ? 'bg-status-success-bg text-status-success-
|
|
1
|
+
{"version":3,"file":"ProvidersTab.js","names":[],"sources":["../../../src/pages/tunnels/ProvidersTab.tsx"],"sourcesContent":["import { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { useListTunnelProvidersQuery } from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport function ProvidersTab() {\n const tr = useTr();\n const { data, loading, error, refetch } = useListTunnelProvidersQuery({\n fetchPolicy: 'cache-and-network',\n });\n\n // Show VibeTunnels first (preferred default), then Cloudflare, then any\n // future providers alphabetically. This matches the ordering used in the\n // tunnels list filter and the create wizard's provider selector.\n const providers = [...(data?.tunnelProviders ?? [])].sort(\n (a: { key: string }, b: { key: string }) => {\n const rank = (key: string) => (key === 'VIBETUNNELS' ? 0 : key === 'CLOUDFLARE' ? 1 : 2);\n const diff = rank(a.key) - rank(b.key);\n if (diff !== 0) return diff;\n return a.key.localeCompare(b.key);\n }\n );\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.providers.title', 'Providers')}\n description={tr(\n 'vibecontrols.tunnels.providers.description',\n 'Tunnel providers registered for this workspace with their capabilities.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading providers\" />}\n {error && (\n <ErrorState\n title=\"Failed to load providers\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && providers.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.providers.emptyTitle', 'No providers registered')}\n description={tr(\n 'vibecontrols.tunnels.providers.emptyDescription',\n 'Contact your workspace admin to enable VibeTunnels or Cloudflare providers.'\n )}\n />\n )}\n\n {data && providers.length > 0 && (\n <div className=\"grid gap-4 md:grid-cols-2\">\n {providers.map((provider: any) => {\n const caps = provider.capabilities;\n return (\n <div\n key={provider.id}\n className={`rounded-lg border p-4 ${\n provider.isEnabled\n ? 'border-border-default bg-bg-surface'\n : 'border-border-subtle bg-bg-sunken opacity-60'\n }`}\n >\n <div className=\"flex items-start justify-between gap-3 mb-3\">\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {provider.displayName}\n </h3>\n <p className=\"text-xs text-text-muted\">{provider.key}</p>\n </div>\n <span\n className={`rounded-full px-2 py-0.5 text-xs ${\n provider.isEnabled\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {provider.isEnabled ? 'Enabled' : 'Disabled'}\n </span>\n </div>\n {provider.description && (\n <p className=\"text-sm text-text-secondary mb-3\">{provider.description}</p>\n )}\n <dl className=\"grid grid-cols-2 gap-1 text-xs\">\n <Capability label=\"HTTP\" value={caps.supportsHttp} />\n <Capability label=\"HTTPS\" value={caps.supportsHttps} />\n <Capability label=\"TCP\" value={caps.supportsTcp} />\n <Capability label=\"Custom domains\" value={caps.supportsCustomDomains} />\n <Capability label=\"Managed subdomains\" value={caps.supportsManagedSubdomains} />\n <Capability label=\"Live logs\" value={caps.supportsLiveLogs} />\n <Capability label=\"Usage metrics\" value={caps.supportsUsageMetrics} />\n <Capability label=\"Credential rotation\" value={caps.supportsRotateCredentials} />\n </dl>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\nfunction Capability({ label, value }: { label: string; value: boolean }) {\n return (\n <div className=\"flex items-center gap-2\">\n <span\n className={`inline-block size-2 rounded-full ${\n value ? 'bg-status-success-bg' : 'bg-border-default'\n }`}\n />\n <dt className=\"text-text-secondary\">{label}</dt>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,IAAe;CAC7B,IAAM,IAAK,GAAO,EACZ,EAAE,SAAM,YAAS,UAAO,eAAY,EAA4B,EACpE,aAAa,qBACd,CAAC,EAKI,IAAY,CAAC,GAAI,GAAM,mBAAmB,EAAE,CAAE,CAAC,MAClD,GAAoB,MAAuB;EAC1C,IAAM,KAAQ,MAAiB,MAAQ,gBAAgB,IAAI,MAAQ,eAAe,IAAI,GAChF,IAAO,EAAK,EAAE,IAAI,GAAG,EAAK,EAAE,IAAI;AAEtC,SADI,MAAS,IACN,EAAE,IAAI,cAAc,EAAE,IAAI,GADV;GAG1B;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAO,EAAG,wCAAwC,YAAY;IAC9D,aAAa,EACX,8CACA,0EACD;IACD,CAAA;GAED,KAAW,CAAC,KAAQ,kBAAC,GAAD,EAAc,SAAQ,qBAAsB,CAAA;GAChE,KACC,kBAAC,GAAD;IACE,OAAM;IACN,SAAS,EAAM;IACf,eAAe,GAAS;IACxB,CAAA;GAGH,KAAQ,EAAU,WAAW,KAC5B,kBAAC,GAAD;IACE,OAAO,EAAG,6CAA6C,0BAA0B;IACjF,aAAa,EACX,mDACA,8EACD;IACD,CAAA;GAGH,KAAQ,EAAU,SAAS,KAC1B,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAU,KAAK,MAAkB;KAChC,IAAM,IAAO,EAAS;AACtB,YACE,kBAAC,OAAD;MAEE,WAAW,yBACT,EAAS,YACL,wCACA;gBALR;OAQE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAS;SACP,CAAA,EACL,kBAAC,KAAD;SAAG,WAAU;mBAA2B,EAAS;SAAQ,CAAA,CACrD,EAAA,CAAA,EACN,kBAAC,QAAD;SACE,WAAW,oCACT,EAAS,YACL,yDACA;mBAGL,EAAS,YAAY,YAAY;SAC7B,CAAA,CACH;;OACL,EAAS,eACR,kBAAC,KAAD;QAAG,WAAU;kBAAoC,EAAS;QAAgB,CAAA;OAE5E,kBAAC,MAAD;QAAI,WAAU;kBAAd;SACE,kBAAC,GAAD;UAAY,OAAM;UAAO,OAAO,EAAK;UAAgB,CAAA;SACrD,kBAAC,GAAD;UAAY,OAAM;UAAQ,OAAO,EAAK;UAAiB,CAAA;SACvD,kBAAC,GAAD;UAAY,OAAM;UAAM,OAAO,EAAK;UAAe,CAAA;SACnD,kBAAC,GAAD;UAAY,OAAM;UAAiB,OAAO,EAAK;UAAyB,CAAA;SACxE,kBAAC,GAAD;UAAY,OAAM;UAAqB,OAAO,EAAK;UAA6B,CAAA;SAChF,kBAAC,GAAD;UAAY,OAAM;UAAY,OAAO,EAAK;UAAoB,CAAA;SAC9D,kBAAC,GAAD;UAAY,OAAM;UAAgB,OAAO,EAAK;UAAwB,CAAA;SACtE,kBAAC,GAAD;UAAY,OAAM;UAAsB,OAAO,EAAK;UAA6B,CAAA;SAC9E;;OACD;QArCC,EAAS,GAqCV;MAER;IACE,CAAA;GAEJ;;;AAIV,SAAS,EAAW,EAAE,UAAO,YAA4C;AACvE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,QAAD,EACE,WAAW,oCACT,IAAQ,yBAAyB,uBAEnC,CAAA,EACF,kBAAC,MAAD;GAAI,WAAU;aAAuB;GAAW,CAAA,CAC5C"}
|
|
@@ -89,7 +89,7 @@ function l() {
|
|
|
89
89
|
/* @__PURE__ */ o("td", {
|
|
90
90
|
className: "px-4 py-3",
|
|
91
91
|
children: /* @__PURE__ */ o("span", {
|
|
92
|
-
className: `inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${e.status === "CONNECTED" ? "bg-status-success-bg text-status-success-
|
|
92
|
+
className: `inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${e.status === "CONNECTED" ? "bg-status-success-bg-subtle text-status-success-text" : "bg-bg-sunken text-text-muted"}`,
|
|
93
93
|
children: e.status
|
|
94
94
|
})
|
|
95
95
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionsTab.js","names":[],"sources":["../../../src/pages/tunnels/SessionsTab.tsx"],"sourcesContent":["import { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport {\n useListActiveTunnelsWithSessionsQuery,\n type ListActiveTunnelsWithSessionsQuery,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype ActiveTunnel = ListActiveTunnelsWithSessionsQuery['tunnels']['items'][number];\ntype ActiveSession = ActiveTunnel['activeSessions'][number] & {\n tunnelName: string;\n providerKey: ActiveTunnel['providerKey'];\n};\n\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;\n return `${(n / 1024 / 1024).toFixed(1)} MiB`;\n}\n\nexport function SessionsTab() {\n const tr = useTr();\n const { data, loading, error, refetch } = useListActiveTunnelsWithSessionsQuery({\n variables: { pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const tunnels = data?.tunnels?.items ?? [];\n const flatSessions: ActiveSession[] = tunnels.flatMap((t: ActiveTunnel) =>\n t.activeSessions.map((s) => ({\n ...s,\n tunnelName: t.name,\n providerKey: t.providerKey,\n }))\n );\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.sessions.title', 'Sessions')}\n description={tr(\n 'vibecontrols.tunnels.sessions.description',\n 'Active sessions across every tunnel in the workspace.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading sessions\" />}\n {error && (\n <ErrorState\n title=\"Failed to load sessions\"\n message={error.message}\n onRetry={() => void refetch()}\n />\n )}\n\n {data && flatSessions.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.sessions.emptyTitle', 'No active sessions')}\n description={tr(\n 'vibecontrols.tunnels.sessions.emptyDescription',\n 'Start a tunnel to issue a session.'\n )}\n />\n )}\n\n {flatSessions.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n <table className=\"w-full text-sm min-w-[640px]\">\n <thead className=\"border-b border-border-subtle bg-bg-sunken text-text-secondary\">\n <tr>\n <th className=\"px-4 py-3 text-left font-medium\">Tunnel</th>\n <th className=\"px-4 py-3 text-left font-medium\">Fingerprint</th>\n <th className=\"px-4 py-3 text-left font-medium\">Status</th>\n <th className=\"px-4 py-3 text-left font-medium whitespace-nowrap\">Connected</th>\n <th className=\"px-4 py-3 text-left font-medium whitespace-nowrap\">Client IP</th>\n <th className=\"px-4 py-3 text-right font-medium whitespace-nowrap\">Bytes ↓/↑</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {flatSessions.map((s: ActiveSession) => (\n <tr key={s.id} className=\"hover:bg-bg-sunken\">\n <td className=\"px-4 py-3\">\n <div className=\"text-text-primary font-medium whitespace-nowrap\">\n {s.tunnelName}\n </div>\n <div className=\"text-xs text-text-muted\">{s.providerKey}</div>\n </td>\n <td className=\"px-4 py-3 font-mono text-xs text-text-primary whitespace-nowrap\">\n {s.tokenFingerprint}\n </td>\n <td className=\"px-4 py-3\">\n <span\n className={`inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${\n s.status === 'CONNECTED'\n ? 'bg-status-success-bg text-status-success-
|
|
1
|
+
{"version":3,"file":"SessionsTab.js","names":[],"sources":["../../../src/pages/tunnels/SessionsTab.tsx"],"sourcesContent":["import { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport {\n useListActiveTunnelsWithSessionsQuery,\n type ListActiveTunnelsWithSessionsQuery,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype ActiveTunnel = ListActiveTunnelsWithSessionsQuery['tunnels']['items'][number];\ntype ActiveSession = ActiveTunnel['activeSessions'][number] & {\n tunnelName: string;\n providerKey: ActiveTunnel['providerKey'];\n};\n\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;\n return `${(n / 1024 / 1024).toFixed(1)} MiB`;\n}\n\nexport function SessionsTab() {\n const tr = useTr();\n const { data, loading, error, refetch } = useListActiveTunnelsWithSessionsQuery({\n variables: { pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const tunnels = data?.tunnels?.items ?? [];\n const flatSessions: ActiveSession[] = tunnels.flatMap((t: ActiveTunnel) =>\n t.activeSessions.map((s) => ({\n ...s,\n tunnelName: t.name,\n providerKey: t.providerKey,\n }))\n );\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.sessions.title', 'Sessions')}\n description={tr(\n 'vibecontrols.tunnels.sessions.description',\n 'Active sessions across every tunnel in the workspace.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading sessions\" />}\n {error && (\n <ErrorState\n title=\"Failed to load sessions\"\n message={error.message}\n onRetry={() => void refetch()}\n />\n )}\n\n {data && flatSessions.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.sessions.emptyTitle', 'No active sessions')}\n description={tr(\n 'vibecontrols.tunnels.sessions.emptyDescription',\n 'Start a tunnel to issue a session.'\n )}\n />\n )}\n\n {flatSessions.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n <table className=\"w-full text-sm min-w-[640px]\">\n <thead className=\"border-b border-border-subtle bg-bg-sunken text-text-secondary\">\n <tr>\n <th className=\"px-4 py-3 text-left font-medium\">Tunnel</th>\n <th className=\"px-4 py-3 text-left font-medium\">Fingerprint</th>\n <th className=\"px-4 py-3 text-left font-medium\">Status</th>\n <th className=\"px-4 py-3 text-left font-medium whitespace-nowrap\">Connected</th>\n <th className=\"px-4 py-3 text-left font-medium whitespace-nowrap\">Client IP</th>\n <th className=\"px-4 py-3 text-right font-medium whitespace-nowrap\">Bytes ↓/↑</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {flatSessions.map((s: ActiveSession) => (\n <tr key={s.id} className=\"hover:bg-bg-sunken\">\n <td className=\"px-4 py-3\">\n <div className=\"text-text-primary font-medium whitespace-nowrap\">\n {s.tunnelName}\n </div>\n <div className=\"text-xs text-text-muted\">{s.providerKey}</div>\n </td>\n <td className=\"px-4 py-3 font-mono text-xs text-text-primary whitespace-nowrap\">\n {s.tokenFingerprint}\n </td>\n <td className=\"px-4 py-3\">\n <span\n className={`inline-block rounded-full px-2 py-0.5 text-xs whitespace-nowrap ${\n s.status === 'CONNECTED'\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {s.status}\n </span>\n </td>\n <td className=\"px-4 py-3 text-xs text-text-muted whitespace-nowrap\">\n {s.connectedAt ? new Date(s.connectedAt).toLocaleString() : '—'}\n </td>\n <td className=\"px-4 py-3 text-xs text-text-muted font-mono\">\n {s.clientIp ?? '—'}\n </td>\n <td className=\"px-4 py-3 text-right text-xs text-text-muted whitespace-nowrap\">\n {formatBytes(Number(s.bytesIn))} / {formatBytes(Number(s.bytesOut))}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAgBA,SAAS,EAAY,GAAmB;AAGtC,QAFI,IAAI,OAAa,GAAG,EAAE,MACtB,IAAI,OAAO,OAAa,IAAI,IAAI,MAAM,QAAQ,EAAE,CAAC,QAC9C,IAAI,IAAI,OAAO,MAAM,QAAQ,EAAE,CAAC;;AAGzC,SAAgB,IAAc;CAC5B,IAAM,IAAK,GAAO,EACZ,EAAE,SAAM,YAAS,UAAO,eAAY,EAAsC;EAC9E,WAAW,EAAE,YAAY,EAAE,OAAO,IAAI,EAAE;EACxC,aAAa;EACd,CAAC,EAGI,KADU,GAAM,SAAS,SAAS,EAAE,EACI,SAAS,MACrD,EAAE,eAAe,KAAK,OAAO;EAC3B,GAAG;EACH,YAAY,EAAE;EACd,aAAa,EAAE;EAChB,EAAE,CACJ;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAO,EAAG,uCAAuC,WAAW;IAC5D,aAAa,EACX,6CACA,wDACD;IACD,CAAA;GAED,KAAW,CAAC,KAAQ,kBAAC,GAAD,EAAc,SAAQ,oBAAqB,CAAA;GAC/D,KACC,kBAAC,GAAD;IACE,OAAM;IACN,SAAS,EAAM;IACf,eAAe,KAAK,GAAS;IAC7B,CAAA;GAGH,KAAQ,EAAa,WAAW,KAC/B,kBAAC,GAAD;IACE,OAAO,EAAG,4CAA4C,qBAAqB;IAC3E,aAAa,EACX,kDACA,qCACD;IACD,CAAA;GAGH,EAAa,SAAS,KACrB,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD;MAAO,WAAU;gBACf,kBAAC,MAAD,EAAA,UAAA;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAkC;QAAW,CAAA;OAC3D,kBAAC,MAAD;QAAI,WAAU;kBAAkC;QAAgB,CAAA;OAChE,kBAAC,MAAD;QAAI,WAAU;kBAAkC;QAAW,CAAA;OAC3D,kBAAC,MAAD;QAAI,WAAU;kBAAoD;QAAc,CAAA;OAChF,kBAAC,MAAD;QAAI,WAAU;kBAAoD;QAAc,CAAA;OAChF,kBAAC,MAAD;QAAI,WAAU;kBAAqD;QAAc,CAAA;OAC9E,EAAA,CAAA;MACC,CAAA,EACR,kBAAC,SAAD;MAAO,WAAU;gBACd,EAAa,KAAK,MACjB,kBAAC,MAAD;OAAe,WAAU;iBAAzB;QACE,kBAAC,MAAD;SAAI,WAAU;mBAAd,CACE,kBAAC,OAAD;UAAK,WAAU;oBACZ,EAAE;UACC,CAAA,EACN,kBAAC,OAAD;UAAK,WAAU;oBAA2B,EAAE;UAAkB,CAAA,CAC3D;;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE;SACA,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,QAAD;UACE,WAAW,mEACT,EAAE,WAAW,cACT,yDACA;oBAGL,EAAE;UACE,CAAA;SACJ,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,cAAc,IAAI,KAAK,EAAE,YAAY,CAAC,gBAAgB,GAAG;SACzD,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,YAAY;SACZ,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACG,EAAY,OAAO,EAAE,QAAQ,CAAC;UAAC;UAAI,EAAY,OAAO,EAAE,SAAS,CAAC;UAChE;;QACF;SA9BI,EAAE,GA8BN,CACL;MACI,CAAA,CACF;;IACJ,CAAA;GAEJ"}
|
|
@@ -203,7 +203,7 @@ function S() {
|
|
|
203
203
|
})
|
|
204
204
|
}),
|
|
205
205
|
X.status === "ERROR" && X.lastError && /* @__PURE__ */ b("div", {
|
|
206
|
-
className: "mt-3 flex items-start gap-2 rounded-
|
|
206
|
+
className: "mt-3 flex items-start gap-2 rounded-card border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
207
207
|
children: [
|
|
208
208
|
/* @__PURE__ */ y(ge, {
|
|
209
209
|
className: "mt-0.5 size-4 flex-shrink-0",
|
|
@@ -215,14 +215,14 @@ function S() {
|
|
|
215
215
|
className: "font-medium",
|
|
216
216
|
children: S("vibecontrols.tunnels.detail.startFailed", "The tunnel failed to start")
|
|
217
217
|
}), /* @__PURE__ */ y("p", {
|
|
218
|
-
className: "mt-0.5 break-words text-status-error-
|
|
218
|
+
className: "mt-0.5 break-words text-status-error-text",
|
|
219
219
|
children: X.lastError
|
|
220
220
|
})]
|
|
221
221
|
}),
|
|
222
222
|
$ && k && /* @__PURE__ */ y("button", {
|
|
223
223
|
type: "button",
|
|
224
224
|
onClick: () => void G(),
|
|
225
|
-
className: "flex-shrink-0 rounded-
|
|
225
|
+
className: "flex-shrink-0 rounded-button border border-status-error-border px-2.5 py-1 text-xs font-medium text-status-error-text hover:bg-bg-sunken transition-colors",
|
|
226
226
|
children: S("vibecontrols.tunnels.detail.retry", "Retry")
|
|
227
227
|
})
|
|
228
228
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TunnelDetailsPage.js","names":[],"sources":["../../../src/pages/tunnels/TunnelDetailsPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams, useNavigate, useSearchParams, useLocation } from 'react-router-dom';\nimport { Tabs, type TabItem } from '@burdenoff/fe-libs/chrome';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\nimport { Tag, ArrowLeft, Play, Square, Loader2, AlertCircle } from 'lucide-react';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { ConfirmDialog } from '@/components/shared/ConfirmDialog';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport { TunnelStatusBadge } from '@/components/tunnels/TunnelStatusBadge';\nimport { ProviderBadge } from '@/components/tunnels/ProviderBadge';\nimport { EntityShareButton } from '@/components/sharing';\nimport {\n useGetTunnelQuery,\n useStartTunnelMutation,\n useStopTunnelMutation,\n} from '@/generated/wspace-operations';\nimport type { TunnelStatus } from '@/generated/wspace-types';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n} from '@/utils/quotaUtils';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport { AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport { TunnelOverviewTab } from './tabs/TunnelOverviewTab';\nimport { TunnelEndpointsTab } from './tabs/TunnelEndpointsTab';\nimport { TunnelSessionsTab } from './tabs/TunnelSessionsTab';\nimport { TunnelDomainsTab } from './tabs/TunnelDomainsTab';\nimport { TunnelUsageTab } from './tabs/TunnelUsageTab';\nimport { TunnelCliTab } from './tabs/TunnelCliTab';\nimport { TunnelAuditTab } from './tabs/TunnelAuditTab';\nimport { TunnelSettingsTab } from './tabs/TunnelSettingsTab';\n\nexport function TunnelDetailsPage() {\n const tr = useTr();\n // Tunnel reads are gated on `tunnel:read`; start/stop are `tunnel:execute`\n // (both seeded in wspace-rbac-svc). Skip the query when the user can't read\n // so we show a clean denied state instead of a guaranteed 403.\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const canRead = hasPermission(VIBE_PERMISSIONS.TUNNEL_READ);\n const canCreate = hasPermission(VIBE_PERMISSIONS.TUNNEL_CREATE);\n const canUpdate = hasPermission(VIBE_PERMISSIONS.TUNNEL_UPDATE);\n const canDelete = hasPermission(VIBE_PERMISSIONS.TUNNEL_DELETE);\n const canExecute = hasPermission(VIBE_PERMISSIONS.TUNNEL_EXECUTE);\n const { tunnelId = '' } = useParams();\n const navigate = useNavigate();\n const location = useLocation();\n const bus = useEventBus();\n const { workspaceId } = useVibeControls();\n const [params, setParams] = useSearchParams();\n const [isPending, setIsPending] = useState(false);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n // Set by the create wizard so a freshly created tunnel offers to start\n // immediately instead of leaving the user on a DRAFT with no obvious action.\n const [showStartPrompt, setShowStartPrompt] = useState(\n () => (location.state as { autoPromptStart?: boolean } | null)?.autoPromptStart === true\n );\n\n const { data, loading, error, refetch, startPolling, stopPolling } = useGetTunnelQuery({\n variables: { id: tunnelId },\n fetchPolicy: 'cache-and-network',\n skip: !tunnelId || permissionsLoading || !canRead,\n });\n\n // Clear the one-shot auto-prompt flag from history so a refresh / back-forward\n // navigation doesn't re-open the dialog.\n useEffect(() => {\n if ((location.state as { autoPromptStart?: boolean } | null)?.autoPromptStart) {\n navigate(location.pathname + location.search, { replace: true, state: {} });\n }\n // Only run once on mount — intentionally not reacting to later nav changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [startTunnel] = useStartTunnelMutation();\n const [stopTunnel] = useStopTunnelMutation();\n\n const handleStart = async () => {\n const tunnel = data?.tunnel;\n if (!tunnel || isPending || !canExecute) return;\n setShowStartPrompt(false);\n setIsPending(true);\n try {\n const result = await startTunnel({\n variables: { id: tunnel.id },\n optimisticResponse: {\n startTunnel: { ...tunnel, status: 'PROVISIONING' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else if (!result.error) {\n bus.emitCustom('vibecontrols.tunnel.started', {\n workspaceId: workspaceId ?? undefined,\n tunnelId: tunnel.id,\n });\n }\n } catch (err) {\n // Only the quota dialog is reserved for quota errors; everything else is\n // surfaced via the tunnel's lastError banner after the refetch.\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else {\n console.error('[TunnelDetailsPage] startTunnel failed', err);\n }\n } finally {\n setIsPending(false);\n }\n };\n\n const handleStop = async () => {\n const tunnel = data?.tunnel;\n if (!tunnel || isPending || !canExecute) return;\n setIsPending(true);\n try {\n await stopTunnel({\n variables: { id: tunnel.id },\n optimisticResponse: {\n stopTunnel: { ...tunnel, status: 'STOPPED' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n } finally {\n setIsPending(false);\n }\n };\n\n // Poll while transitioning OR ACTIVE-but-no-domain (Cloudflare URL arrives async).\n const needsPoll =\n data?.tunnel?.status === 'PROVISIONING' ||\n data?.tunnel?.status === 'DELETING' ||\n (data?.tunnel?.status === 'ACTIVE' && !data.tunnel.primaryDomain?.hostname);\n useEffect(() => {\n if (needsPoll) {\n startPolling(4000);\n } else {\n stopPolling();\n }\n }, [needsPoll, startPolling, stopPolling]);\n\n const tabs: TabItem[] = useMemo(() => {\n const baseTabs: TabItem[] = [\n { id: 'overview', label: tr('vibecontrols.tunnels.detail.overview', 'Overview') },\n { id: 'endpoints', label: tr('vibecontrols.tunnels.detail.endpoints', 'Endpoints') },\n { id: 'sessions', label: tr('vibecontrols.tunnels.detail.sessions', 'Sessions') },\n { id: 'domains', label: tr('vibecontrols.tunnels.detail.domains', 'Domains') },\n { id: 'usage', label: tr('vibecontrols.tunnels.detail.usage', 'Usage') },\n { id: 'cli', label: tr('vibecontrols.tunnels.detail.cli', 'CLI') },\n { id: 'audit', label: tr('vibecontrols.tunnels.detail.audit', 'Audit') },\n ];\n\n if (canUpdate || canDelete) {\n baseTabs.push({\n id: 'settings',\n label: tr('vibecontrols.tunnels.detail.settings', 'Settings'),\n });\n }\n\n return baseTabs;\n }, [canDelete, canUpdate, tr]);\n\n const requestedTab = params.get('tab');\n const activeId =\n requestedTab && tabs.some((tab) => tab.id === requestedTab) ? requestedTab : tabs[0].id;\n\n const handleTabChange = (id: string) => {\n const next = new URLSearchParams(params);\n if (id === tabs[0].id) next.delete('tab');\n else next.set('tab', id);\n setParams(next, { replace: true });\n };\n\n if (permissionsLoading) return <LoadingState message=\"Loading permissions\" />;\n\n if (!canRead) {\n return (\n <AccessDenied\n title={tr('vibecontrols.tunnels.detail.denied.title', 'Access Denied')}\n description={tr(\n 'vibecontrols.tunnels.detail.denied.description',\n 'You do not have permission to view this tunnel.'\n )}\n />\n );\n }\n if (loading && !data) return <LoadingState message=\"Loading tunnel\" />;\n if (error) {\n return (\n <ErrorState title=\"Failed to load tunnel\" message={error.message} onRetry={() => refetch()} />\n );\n }\n if (!data?.tunnel) {\n return (\n <ErrorState\n title=\"Tunnel not found\"\n message=\"This tunnel may have been deleted.\"\n onRetry={() => navigate('/vibecontrols/tunnels')}\n />\n );\n }\n\n const tunnel = data.tunnel;\n const isTransitioning =\n isPending || tunnel.status === 'PROVISIONING' || tunnel.status === 'DELETING';\n const isActive = tunnel.status === 'ACTIVE';\n const canStop = isActive && !isTransitioning;\n const canStart = !isActive && !isTransitioning;\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"px-6 pt-6 pb-2 border-b border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => navigate('/vibecontrols/tunnels')}\n className=\"flex items-center gap-1.5 mb-4 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft size={16} />\n Back to Tunnels\n </button>\n <PageHeader\n breadcrumb={[{ label: 'Tunnels', path: '/vibecontrols/tunnels' }, { label: tunnel.name }]}\n title={tunnel.name}\n description={\n tunnel.description ?? `${tunnel.protocol}:${tunnel.localPort} on ${tunnel.localHost}`\n }\n actions={\n <div className=\"flex items-center gap-2\">\n {isTransitioning && (\n <span className=\"inline-flex items-center rounded-md border border-border-default px-3 py-2 text-sm text-text-muted\">\n <Loader2 className=\"size-4 mr-1.5 animate-spin\" />\n {isActive\n ? tr('vibecontrols.tunnels.stopping', 'Stopping…')\n : tr('vibecontrols.tunnels.starting', 'Starting…')}\n </span>\n )}\n {canStart && canExecute && (\n <button\n type=\"button\"\n onClick={() => void handleStart()}\n className=\"inline-flex items-center rounded-md bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-fg hover:bg-action-primary-bg-hover focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n <Play className=\"size-4 mr-1.5\" />\n {tr('vibecontrols.tunnels.detail.startTunnel', 'Start tunnel')}\n </button>\n )}\n {canStop && canExecute && (\n <button\n type=\"button\"\n onClick={() => void handleStop()}\n className=\"inline-flex items-center rounded-md border border-border-default px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-elevated focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n <Square className=\"size-4 mr-1.5\" />\n {tr('vibecontrols.tunnels.detail.stopTunnel', 'Stop tunnel')}\n </button>\n )}\n <TunnelStatusBadge status={tunnel.status} />\n <ProviderBadge providerKey={tunnel.providerKey} />\n <EntityShareButton\n entity=\"tunnel\"\n resource={{ id: tunnel.id, name: tunnel.name }}\n label={tr('vibecontrols.tunnels.detail.share', 'Share')}\n variant=\"button\"\n />\n </div>\n }\n />\n {tunnel.status === 'ERROR' && tunnel.lastError && (\n <div className=\"mt-3 flex items-start gap-2 rounded-md bg-status-error-bg px-3 py-2 text-sm text-status-error-fg\">\n <AlertCircle className=\"mt-0.5 size-4 flex-shrink-0\" aria-hidden=\"true\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium\">\n {tr('vibecontrols.tunnels.detail.startFailed', 'The tunnel failed to start')}\n </p>\n <p className=\"mt-0.5 break-words text-status-error-fg/90\">{tunnel.lastError}</p>\n </div>\n {canStart && canExecute && (\n <button\n type=\"button\"\n onClick={() => void handleStart()}\n className=\"flex-shrink-0 rounded-md border border-status-error-fg/30 px-2.5 py-1 text-xs font-medium hover:bg-status-error-bg/60\"\n >\n {tr('vibecontrols.tunnels.detail.retry', 'Retry')}\n </button>\n )}\n </div>\n )}\n <div className=\"mt-2\">\n <Tabs\n items={tabs}\n activeTabId={activeId}\n onTabChange={handleTabChange}\n variant=\"underline\"\n />\n </div>\n </div>\n <div className=\"flex items-start gap-2 border-b border-border-subtle bg-bg-surface px-6 py-2\">\n <Tag className=\"mt-1 size-4 flex-shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <div className=\"min-w-0 flex-1\">\n <EntityTagPicker entityType=\"VIBECONTROLS_TUNNEL\" entityId={tunnel.id} />\n </div>\n </div>\n <div className=\"flex-1 min-h-0 overflow-auto p-6\">\n {activeId === 'overview' && (\n <TunnelOverviewTab\n tunnel={tunnel}\n canUpdate={canUpdate}\n onChanged={() => void refetch()}\n />\n )}\n {activeId === 'endpoints' && <TunnelEndpointsTab tunnel={tunnel} />}\n {activeId === 'sessions' && (\n <TunnelSessionsTab tunnelId={tunnel.id} canExecute={canExecute} />\n )}\n {activeId === 'domains' && (\n <TunnelDomainsTab tunnel={tunnel} canCreate={canCreate} canUpdate={canUpdate} />\n )}\n {activeId === 'usage' && <TunnelUsageTab tunnelId={tunnel.id} />}\n {activeId === 'cli' && <TunnelCliTab tunnel={tunnel} />}\n {activeId === 'audit' && <TunnelAuditTab tunnelId={tunnel.id} />}\n {activeId === 'settings' && (\n <TunnelSettingsTab\n tunnel={tunnel}\n canUpdate={canUpdate}\n canDelete={canDelete}\n onDeleted={() => navigate('/vibecontrols/tunnels')}\n />\n )}\n </div>\n\n <ConfirmDialog\n isOpen={showStartPrompt && canStart && canExecute}\n onClose={() => setShowStartPrompt(false)}\n onConfirm={() => void handleStart()}\n title={tr('vibecontrols.tunnels.detail.startPromptTitle', 'Start this tunnel now?')}\n message={tr(\n 'vibecontrols.tunnels.detail.startPromptMessage',\n 'Your tunnel was created as a draft. Start it to bring it online and get its public URL. You can stop it anytime from this page.'\n )}\n confirmLabel={tr('vibecontrols.tunnels.detail.startTunnel', 'Start tunnel')}\n cancelLabel={tr('vibecontrols.tunnels.detail.startPromptLater', 'Not now')}\n variant=\"default\"\n isLoading={isPending}\n />\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing/plans');\n }}\n {...getQuotaDialogContent('tunnel-start')}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TunnelDetailsPage.js","names":[],"sources":["../../../src/pages/tunnels/TunnelDetailsPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams, useNavigate, useSearchParams, useLocation } from 'react-router-dom';\nimport { Tabs, type TabItem } from '@burdenoff/fe-libs/chrome';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\nimport { Tag, ArrowLeft, Play, Square, Loader2, AlertCircle } from 'lucide-react';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { ConfirmDialog } from '@/components/shared/ConfirmDialog';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport { TunnelStatusBadge } from '@/components/tunnels/TunnelStatusBadge';\nimport { ProviderBadge } from '@/components/tunnels/ProviderBadge';\nimport { EntityShareButton } from '@/components/sharing';\nimport {\n useGetTunnelQuery,\n useStartTunnelMutation,\n useStopTunnelMutation,\n} from '@/generated/wspace-operations';\nimport type { TunnelStatus } from '@/generated/wspace-types';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n} from '@/utils/quotaUtils';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport { AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport { TunnelOverviewTab } from './tabs/TunnelOverviewTab';\nimport { TunnelEndpointsTab } from './tabs/TunnelEndpointsTab';\nimport { TunnelSessionsTab } from './tabs/TunnelSessionsTab';\nimport { TunnelDomainsTab } from './tabs/TunnelDomainsTab';\nimport { TunnelUsageTab } from './tabs/TunnelUsageTab';\nimport { TunnelCliTab } from './tabs/TunnelCliTab';\nimport { TunnelAuditTab } from './tabs/TunnelAuditTab';\nimport { TunnelSettingsTab } from './tabs/TunnelSettingsTab';\n\nexport function TunnelDetailsPage() {\n const tr = useTr();\n // Tunnel reads are gated on `tunnel:read`; start/stop are `tunnel:execute`\n // (both seeded in wspace-rbac-svc). Skip the query when the user can't read\n // so we show a clean denied state instead of a guaranteed 403.\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const canRead = hasPermission(VIBE_PERMISSIONS.TUNNEL_READ);\n const canCreate = hasPermission(VIBE_PERMISSIONS.TUNNEL_CREATE);\n const canUpdate = hasPermission(VIBE_PERMISSIONS.TUNNEL_UPDATE);\n const canDelete = hasPermission(VIBE_PERMISSIONS.TUNNEL_DELETE);\n const canExecute = hasPermission(VIBE_PERMISSIONS.TUNNEL_EXECUTE);\n const { tunnelId = '' } = useParams();\n const navigate = useNavigate();\n const location = useLocation();\n const bus = useEventBus();\n const { workspaceId } = useVibeControls();\n const [params, setParams] = useSearchParams();\n const [isPending, setIsPending] = useState(false);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n // Set by the create wizard so a freshly created tunnel offers to start\n // immediately instead of leaving the user on a DRAFT with no obvious action.\n const [showStartPrompt, setShowStartPrompt] = useState(\n () => (location.state as { autoPromptStart?: boolean } | null)?.autoPromptStart === true\n );\n\n const { data, loading, error, refetch, startPolling, stopPolling } = useGetTunnelQuery({\n variables: { id: tunnelId },\n fetchPolicy: 'cache-and-network',\n skip: !tunnelId || permissionsLoading || !canRead,\n });\n\n // Clear the one-shot auto-prompt flag from history so a refresh / back-forward\n // navigation doesn't re-open the dialog.\n useEffect(() => {\n if ((location.state as { autoPromptStart?: boolean } | null)?.autoPromptStart) {\n navigate(location.pathname + location.search, { replace: true, state: {} });\n }\n // Only run once on mount — intentionally not reacting to later nav changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [startTunnel] = useStartTunnelMutation();\n const [stopTunnel] = useStopTunnelMutation();\n\n const handleStart = async () => {\n const tunnel = data?.tunnel;\n if (!tunnel || isPending || !canExecute) return;\n setShowStartPrompt(false);\n setIsPending(true);\n try {\n const result = await startTunnel({\n variables: { id: tunnel.id },\n optimisticResponse: {\n startTunnel: { ...tunnel, status: 'PROVISIONING' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else if (!result.error) {\n bus.emitCustom('vibecontrols.tunnel.started', {\n workspaceId: workspaceId ?? undefined,\n tunnelId: tunnel.id,\n });\n }\n } catch (err) {\n // Only the quota dialog is reserved for quota errors; everything else is\n // surfaced via the tunnel's lastError banner after the refetch.\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else {\n console.error('[TunnelDetailsPage] startTunnel failed', err);\n }\n } finally {\n setIsPending(false);\n }\n };\n\n const handleStop = async () => {\n const tunnel = data?.tunnel;\n if (!tunnel || isPending || !canExecute) return;\n setIsPending(true);\n try {\n await stopTunnel({\n variables: { id: tunnel.id },\n optimisticResponse: {\n stopTunnel: { ...tunnel, status: 'STOPPED' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n } finally {\n setIsPending(false);\n }\n };\n\n // Poll while transitioning OR ACTIVE-but-no-domain (Cloudflare URL arrives async).\n const needsPoll =\n data?.tunnel?.status === 'PROVISIONING' ||\n data?.tunnel?.status === 'DELETING' ||\n (data?.tunnel?.status === 'ACTIVE' && !data.tunnel.primaryDomain?.hostname);\n useEffect(() => {\n if (needsPoll) {\n startPolling(4000);\n } else {\n stopPolling();\n }\n }, [needsPoll, startPolling, stopPolling]);\n\n const tabs: TabItem[] = useMemo(() => {\n const baseTabs: TabItem[] = [\n { id: 'overview', label: tr('vibecontrols.tunnels.detail.overview', 'Overview') },\n { id: 'endpoints', label: tr('vibecontrols.tunnels.detail.endpoints', 'Endpoints') },\n { id: 'sessions', label: tr('vibecontrols.tunnels.detail.sessions', 'Sessions') },\n { id: 'domains', label: tr('vibecontrols.tunnels.detail.domains', 'Domains') },\n { id: 'usage', label: tr('vibecontrols.tunnels.detail.usage', 'Usage') },\n { id: 'cli', label: tr('vibecontrols.tunnels.detail.cli', 'CLI') },\n { id: 'audit', label: tr('vibecontrols.tunnels.detail.audit', 'Audit') },\n ];\n\n if (canUpdate || canDelete) {\n baseTabs.push({\n id: 'settings',\n label: tr('vibecontrols.tunnels.detail.settings', 'Settings'),\n });\n }\n\n return baseTabs;\n }, [canDelete, canUpdate, tr]);\n\n const requestedTab = params.get('tab');\n const activeId =\n requestedTab && tabs.some((tab) => tab.id === requestedTab) ? requestedTab : tabs[0].id;\n\n const handleTabChange = (id: string) => {\n const next = new URLSearchParams(params);\n if (id === tabs[0].id) next.delete('tab');\n else next.set('tab', id);\n setParams(next, { replace: true });\n };\n\n if (permissionsLoading) return <LoadingState message=\"Loading permissions\" />;\n\n if (!canRead) {\n return (\n <AccessDenied\n title={tr('vibecontrols.tunnels.detail.denied.title', 'Access Denied')}\n description={tr(\n 'vibecontrols.tunnels.detail.denied.description',\n 'You do not have permission to view this tunnel.'\n )}\n />\n );\n }\n if (loading && !data) return <LoadingState message=\"Loading tunnel\" />;\n if (error) {\n return (\n <ErrorState title=\"Failed to load tunnel\" message={error.message} onRetry={() => refetch()} />\n );\n }\n if (!data?.tunnel) {\n return (\n <ErrorState\n title=\"Tunnel not found\"\n message=\"This tunnel may have been deleted.\"\n onRetry={() => navigate('/vibecontrols/tunnels')}\n />\n );\n }\n\n const tunnel = data.tunnel;\n const isTransitioning =\n isPending || tunnel.status === 'PROVISIONING' || tunnel.status === 'DELETING';\n const isActive = tunnel.status === 'ACTIVE';\n const canStop = isActive && !isTransitioning;\n const canStart = !isActive && !isTransitioning;\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"px-6 pt-6 pb-2 border-b border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => navigate('/vibecontrols/tunnels')}\n className=\"flex items-center gap-1.5 mb-4 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft size={16} />\n Back to Tunnels\n </button>\n <PageHeader\n breadcrumb={[{ label: 'Tunnels', path: '/vibecontrols/tunnels' }, { label: tunnel.name }]}\n title={tunnel.name}\n description={\n tunnel.description ?? `${tunnel.protocol}:${tunnel.localPort} on ${tunnel.localHost}`\n }\n actions={\n <div className=\"flex items-center gap-2\">\n {isTransitioning && (\n <span className=\"inline-flex items-center rounded-md border border-border-default px-3 py-2 text-sm text-text-muted\">\n <Loader2 className=\"size-4 mr-1.5 animate-spin\" />\n {isActive\n ? tr('vibecontrols.tunnels.stopping', 'Stopping…')\n : tr('vibecontrols.tunnels.starting', 'Starting…')}\n </span>\n )}\n {canStart && canExecute && (\n <button\n type=\"button\"\n onClick={() => void handleStart()}\n className=\"inline-flex items-center rounded-md bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-fg hover:bg-action-primary-bg-hover focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n <Play className=\"size-4 mr-1.5\" />\n {tr('vibecontrols.tunnels.detail.startTunnel', 'Start tunnel')}\n </button>\n )}\n {canStop && canExecute && (\n <button\n type=\"button\"\n onClick={() => void handleStop()}\n className=\"inline-flex items-center rounded-md border border-border-default px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-elevated focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n <Square className=\"size-4 mr-1.5\" />\n {tr('vibecontrols.tunnels.detail.stopTunnel', 'Stop tunnel')}\n </button>\n )}\n <TunnelStatusBadge status={tunnel.status} />\n <ProviderBadge providerKey={tunnel.providerKey} />\n <EntityShareButton\n entity=\"tunnel\"\n resource={{ id: tunnel.id, name: tunnel.name }}\n label={tr('vibecontrols.tunnels.detail.share', 'Share')}\n variant=\"button\"\n />\n </div>\n }\n />\n {tunnel.status === 'ERROR' && tunnel.lastError && (\n <div className=\"mt-3 flex items-start gap-2 rounded-card border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n <AlertCircle className=\"mt-0.5 size-4 flex-shrink-0\" aria-hidden=\"true\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium\">\n {tr('vibecontrols.tunnels.detail.startFailed', 'The tunnel failed to start')}\n </p>\n <p className=\"mt-0.5 break-words text-status-error-text\">{tunnel.lastError}</p>\n </div>\n {canStart && canExecute && (\n <button\n type=\"button\"\n onClick={() => void handleStart()}\n className=\"flex-shrink-0 rounded-button border border-status-error-border px-2.5 py-1 text-xs font-medium text-status-error-text hover:bg-bg-sunken transition-colors\"\n >\n {tr('vibecontrols.tunnels.detail.retry', 'Retry')}\n </button>\n )}\n </div>\n )}\n <div className=\"mt-2\">\n <Tabs\n items={tabs}\n activeTabId={activeId}\n onTabChange={handleTabChange}\n variant=\"underline\"\n />\n </div>\n </div>\n <div className=\"flex items-start gap-2 border-b border-border-subtle bg-bg-surface px-6 py-2\">\n <Tag className=\"mt-1 size-4 flex-shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <div className=\"min-w-0 flex-1\">\n <EntityTagPicker entityType=\"VIBECONTROLS_TUNNEL\" entityId={tunnel.id} />\n </div>\n </div>\n <div className=\"flex-1 min-h-0 overflow-auto p-6\">\n {activeId === 'overview' && (\n <TunnelOverviewTab\n tunnel={tunnel}\n canUpdate={canUpdate}\n onChanged={() => void refetch()}\n />\n )}\n {activeId === 'endpoints' && <TunnelEndpointsTab tunnel={tunnel} />}\n {activeId === 'sessions' && (\n <TunnelSessionsTab tunnelId={tunnel.id} canExecute={canExecute} />\n )}\n {activeId === 'domains' && (\n <TunnelDomainsTab tunnel={tunnel} canCreate={canCreate} canUpdate={canUpdate} />\n )}\n {activeId === 'usage' && <TunnelUsageTab tunnelId={tunnel.id} />}\n {activeId === 'cli' && <TunnelCliTab tunnel={tunnel} />}\n {activeId === 'audit' && <TunnelAuditTab tunnelId={tunnel.id} />}\n {activeId === 'settings' && (\n <TunnelSettingsTab\n tunnel={tunnel}\n canUpdate={canUpdate}\n canDelete={canDelete}\n onDeleted={() => navigate('/vibecontrols/tunnels')}\n />\n )}\n </div>\n\n <ConfirmDialog\n isOpen={showStartPrompt && canStart && canExecute}\n onClose={() => setShowStartPrompt(false)}\n onConfirm={() => void handleStart()}\n title={tr('vibecontrols.tunnels.detail.startPromptTitle', 'Start this tunnel now?')}\n message={tr(\n 'vibecontrols.tunnels.detail.startPromptMessage',\n 'Your tunnel was created as a draft. Start it to bring it online and get its public URL. You can stop it anytime from this page.'\n )}\n confirmLabel={tr('vibecontrols.tunnels.detail.startTunnel', 'Start tunnel')}\n cancelLabel={tr('vibecontrols.tunnels.detail.startPromptLater', 'Not now')}\n variant=\"default\"\n isLoading={isPending}\n />\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing/plans');\n }}\n {...getQuotaDialogContent('tunnel-start')}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -57,7 +57,7 @@ function u() {
|
|
|
57
57
|
}), /* @__PURE__ */ l("div", {
|
|
58
58
|
className: "flex items-center gap-2 flex-shrink-0 ml-4",
|
|
59
59
|
children: [/* @__PURE__ */ c("span", {
|
|
60
|
-
className: `inline-block rounded-full px-2 py-0.5 text-xs ${e.status === "ACTIVE" ? "bg-status-success-bg text-status-success-
|
|
60
|
+
className: `inline-block rounded-full px-2 py-0.5 text-xs ${e.status === "ACTIVE" ? "bg-status-success-bg-subtle text-status-success-text" : e.status === "DEGRADED" ? "bg-status-warning-bg-subtle text-status-warning-text" : "bg-bg-sunken text-text-muted"}`,
|
|
61
61
|
children: e.status
|
|
62
62
|
}), /* @__PURE__ */ c("span", {
|
|
63
63
|
className: "text-xs text-text-muted group-hover:text-text-brand",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsageTab.js","names":[],"sources":["../../../src/pages/tunnels/UsageTab.tsx"],"sourcesContent":["import { useNavigate } from 'react-router-dom';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { useListTunnelsQuery, type ListTunnelsQuery } from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype TunnelItem = ListTunnelsQuery['tunnels']['items'][number];\n\nexport function UsageTab() {\n const tr = useTr();\n const navigate = useNavigate();\n const { data, loading, error, refetch } = useListTunnelsQuery({\n variables: { pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const tunnels = data?.tunnels?.items ?? [];\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.usage.title', 'Usage')}\n description={tr(\n 'vibecontrols.tunnels.usage.description',\n 'Bandwidth, requests, and connections per tunnel. Open a tunnel to see hourly charts.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading tunnels\" />}\n {error && (\n <ErrorState\n title=\"Failed to load tunnels\"\n message={error.message}\n onRetry={() => void refetch()}\n />\n )}\n\n {data && tunnels.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.usage.emptyTitle', 'No tunnels yet')}\n description={tr(\n 'vibecontrols.tunnels.usage.emptyDescription',\n 'Create a tunnel and send traffic through it to see usage metrics.'\n )}\n />\n )}\n\n {tunnels.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-text-muted mb-4\">\n {tr(\n 'vibecontrols.tunnels.usage.hint',\n 'Select a tunnel to view its hourly bandwidth and request counts.'\n )}\n </p>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface divide-y divide-border-subtle\">\n {tunnels.map((tunnel: TunnelItem) => (\n <button\n key={tunnel.id}\n type=\"button\"\n onClick={() => navigate(`/vibecontrols/tunnels/${tunnel.id}?tab=usage`)}\n className=\"w-full flex items-center justify-between px-4 py-3 text-left hover:bg-bg-sunken group\"\n >\n <div className=\"min-w-0\">\n <div className=\"text-sm font-medium text-text-primary group-hover:text-text-brand\">\n {tunnel.name}\n </div>\n <div className=\"text-xs text-text-muted\">\n {tunnel.slug} · {tunnel.providerKey}\n {tunnel.primaryDomain?.hostname && <> · {tunnel.primaryDomain.hostname}</>}\n </div>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0 ml-4\">\n <span\n className={`inline-block rounded-full px-2 py-0.5 text-xs ${\n tunnel.status === 'ACTIVE'\n ? 'bg-status-success-bg text-status-success-
|
|
1
|
+
{"version":3,"file":"UsageTab.js","names":[],"sources":["../../../src/pages/tunnels/UsageTab.tsx"],"sourcesContent":["import { useNavigate } from 'react-router-dom';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { useListTunnelsQuery, type ListTunnelsQuery } from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype TunnelItem = ListTunnelsQuery['tunnels']['items'][number];\n\nexport function UsageTab() {\n const tr = useTr();\n const navigate = useNavigate();\n const { data, loading, error, refetch } = useListTunnelsQuery({\n variables: { pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const tunnels = data?.tunnels?.items ?? [];\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.usage.title', 'Usage')}\n description={tr(\n 'vibecontrols.tunnels.usage.description',\n 'Bandwidth, requests, and connections per tunnel. Open a tunnel to see hourly charts.'\n )}\n />\n\n {loading && !data && <LoadingState message=\"Loading tunnels\" />}\n {error && (\n <ErrorState\n title=\"Failed to load tunnels\"\n message={error.message}\n onRetry={() => void refetch()}\n />\n )}\n\n {data && tunnels.length === 0 && (\n <EmptyState\n title={tr('vibecontrols.tunnels.usage.emptyTitle', 'No tunnels yet')}\n description={tr(\n 'vibecontrols.tunnels.usage.emptyDescription',\n 'Create a tunnel and send traffic through it to see usage metrics.'\n )}\n />\n )}\n\n {tunnels.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-text-muted mb-4\">\n {tr(\n 'vibecontrols.tunnels.usage.hint',\n 'Select a tunnel to view its hourly bandwidth and request counts.'\n )}\n </p>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface divide-y divide-border-subtle\">\n {tunnels.map((tunnel: TunnelItem) => (\n <button\n key={tunnel.id}\n type=\"button\"\n onClick={() => navigate(`/vibecontrols/tunnels/${tunnel.id}?tab=usage`)}\n className=\"w-full flex items-center justify-between px-4 py-3 text-left hover:bg-bg-sunken group\"\n >\n <div className=\"min-w-0\">\n <div className=\"text-sm font-medium text-text-primary group-hover:text-text-brand\">\n {tunnel.name}\n </div>\n <div className=\"text-xs text-text-muted\">\n {tunnel.slug} · {tunnel.providerKey}\n {tunnel.primaryDomain?.hostname && <> · {tunnel.primaryDomain.hostname}</>}\n </div>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0 ml-4\">\n <span\n className={`inline-block rounded-full px-2 py-0.5 text-xs ${\n tunnel.status === 'ACTIVE'\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : tunnel.status === 'DEGRADED'\n ? 'bg-status-warning-bg-subtle text-status-warning-text'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {tunnel.status}\n </span>\n <span className=\"text-xs text-text-muted group-hover:text-text-brand\">\n View usage →\n </span>\n </div>\n </button>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,IAAW;CACzB,IAAM,IAAK,GAAO,EACZ,IAAW,GAAa,EACxB,EAAE,SAAM,YAAS,UAAO,eAAY,EAAoB;EAC5D,WAAW,EAAE,YAAY,EAAE,OAAO,IAAI,EAAE;EACxC,aAAa;EACd,CAAC,EAEI,IAAU,GAAM,SAAS,SAAS,EAAE;AAE1C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAO,EAAG,oCAAoC,QAAQ;IACtD,aAAa,EACX,0CACA,uFACD;IACD,CAAA;GAED,KAAW,CAAC,KAAQ,kBAAC,GAAD,EAAc,SAAQ,mBAAoB,CAAA;GAC9D,KACC,kBAAC,GAAD;IACE,OAAM;IACN,SAAS,EAAM;IACf,eAAe,KAAK,GAAS;IAC7B,CAAA;GAGH,KAAQ,EAAQ,WAAW,KAC1B,kBAAC,GAAD;IACE,OAAO,EAAG,yCAAyC,iBAAiB;IACpE,aAAa,EACX,+CACA,oEACD;IACD,CAAA;GAGH,EAAQ,SAAS,KAChB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,mCACA,mEACD;KACC,CAAA,EACJ,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAQ,KAAK,MACZ,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe,EAAS,yBAAyB,EAAO,GAAG,YAAY;MACvE,WAAU;gBAJZ,CAME,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAO;QACJ,CAAA,EACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACG,EAAO;SAAK;SAAI,EAAO;SACvB,EAAO,eAAe,YAAY,kBAAA,GAAA,EAAA,UAAA,CAAE,OAAI,EAAO,cAAc,SAAY,EAAA,CAAA;SACtE;UACF;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QACE,WAAW,iDACT,EAAO,WAAW,WACd,yDACA,EAAO,WAAW,aAChB,yDACA;kBAGP,EAAO;QACH,CAAA,EACP,kBAAC,QAAD;QAAM,WAAU;kBAAsD;QAE/D,CAAA,CACH;SACC;QA9BF,EAAO,GA8BL,CACT;KACE,CAAA,CACF;;GAEJ"}
|