feat: Add HTTPS/Traefik support with centralized endpoints

- Create traefik/agent-ui.yml with full routing config for domain z590.nucleoriofrio.com
- Add frontend/src/config/endpoints.ts for automatic HTTP/HTTPS detection
- Update all hardcoded localhost URLs to use relative paths
- WebSocket connections auto-detect wss:// vs ws:// based on page protocol
- Configure path-based WebSocket routing (/ws/terminal, /ws/mcp, /ws/status, /ws/whisper)
- Add commented IP whitelist middleware for future security
This commit is contained in:
2026-02-14 03:20:51 -06:00
parent 47f5524416
commit 902029c805
18 changed files with 471 additions and 26 deletions

View File

@@ -11,6 +11,7 @@ import FloatingResponse from './components/FloatingResponse.vue'
import FloatingVoice from './components/FloatingVoice.vue'
import PwaInstallBanner from './components/PwaInstallBanner.vue'
import { initWebMCP, getWebMCP, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp'
import { endpoints } from './config/endpoints'
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
import { setTerminalControls } from './services/tools/handlers/terminalHandlers'
import { setResponseControls } from './services/tools/handlers/responseHandlers'
@@ -48,7 +49,7 @@ let toolFlashTimeout: number | null = null
function connectStatusWs() {
if (statusWs?.readyState === WebSocket.OPEN) return
statusWs = new WebSocket(`ws://${window.location.hostname}:4103`)
statusWs = new WebSocket(endpoints.claudeStatus)
statusWs.onopen = () => {
console.log('[App] Status WebSocket connected')

View File

@@ -6,6 +6,7 @@ import { WebLinksAddon } from '@xterm/addon-web-links'
import '@xterm/xterm/css/xterm.css'
import { connectWithToken, stopTokenPolling } from '../services/webmcp'
import { useCanvasStore } from '../stores/canvas'
import { endpoints } from '../config/endpoints'
const props = defineProps<{
modelValue: boolean
@@ -47,7 +48,7 @@ let tokenBuffer = ''
let tokenTimeout: number | null = null
const waitingForToken = ref(false)
const WS_URL = `ws://${window.location.hostname}:4103`
const WS_URL = endpoints.terminal
// Mouse position tracking for Ctrl+E
const mousePos = ref({ x: 0, y: 0 })

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useCanvasStore } from '../stores/canvas'
import { endpoints } from '../config/endpoints'
const props = defineProps<{
modelValue: boolean
@@ -39,7 +40,7 @@ const containerRef = ref<HTMLElement | null>(null)
let recognition: SpeechRecognition | null = null
// WebSocket connection to terminal
const WS_URL = `ws://${window.location.hostname}:4103`
const WS_URL = endpoints.terminal
let socket: WebSocket | null = null
const connected = ref(false)
@@ -53,7 +54,7 @@ let pendingWhisperSend = false // Flag to send transcript when Whisper responds
const useWhisper = ref(false)
const whisperReady = ref(false)
const whisperLoading = ref(false)
const WHISPER_WS_URL = `ws://${window.location.hostname}:4104`
const WHISPER_WS_URL = endpoints.whisper
let whisperSocket: WebSocket | null = null
let mediaRecorder: MediaRecorder | null = null
let audioChunks: Blob[] = []
@@ -112,7 +113,7 @@ async function saveRecordingToBackend(blob: Blob) {
reader.onloadend = async () => {
const base64 = (reader.result as string).split(',')[1]
const response = await fetch(`http://${window.location.hostname}:4100/api/recordings`, {
const response = await fetch('/api/recordings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -254,7 +255,7 @@ function initRecognition() {
async function checkWhisperStatus(updateLoading = true) {
try {
const res = await fetch(`http://${window.location.hostname}:4100/api/whisper/status`)
const res = await fetch('/api/whisper/status')
const data = await res.json()
useWhisper.value = data.enabled
whisperReady.value = data.running
@@ -288,7 +289,7 @@ async function toggleWhisperMode() {
}
try {
const res = await fetch(`http://${window.location.hostname}:4100/api/whisper/toggle`, {
const res = await fetch('/api/whisper/toggle', {
method: 'POST'
})
const data = await res.json()

View File

@@ -1,7 +1,8 @@
import { ref } from 'vue'
import type { TableInfo, TableSchema, DbStats } from '@/types/database'
const API_BASE = 'http://localhost:4101/api/database'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_BASE = '/api/database'
export function useDatabaseApi() {
const tables = ref<TableInfo[]>([])

View File

@@ -1,6 +1,7 @@
import { ref } from 'vue'
const API_BASE = 'http://localhost:4101/api/database'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_BASE = '/api/database'
export function useQueryExecutor() {
const queryText = ref('')

View File

@@ -0,0 +1,44 @@
/**
* Centralized endpoint configuration for Agent UI
* Automatically detects HTTPS/Traefik and uses appropriate protocols
*/
// Detect if running over HTTPS (behind Traefik)
export const isSecure = window.location.protocol === 'https:'
// WebSocket protocol based on HTTP protocol
export const wsProtocol = isSecure ? 'wss:' : 'ws:'
// Base hostname
export const hostname = window.location.hostname
// Build WebSocket URL based on environment
function buildWsUrl(securePath: string, devPort: number): string {
if (isSecure) {
// Behind Traefik - use path-based routing
return `${wsProtocol}//${hostname}${securePath}`
}
// Development - direct port access
return `${wsProtocol}//${hostname}:${devPort}`
}
// Endpoint configuration
export const endpoints = {
// Terminal WebSocket
terminal: buildWsUrl('/ws/terminal', 4103),
// Claude status WebSocket (same backend as terminal)
claudeStatus: buildWsUrl('/ws/status', 4103),
// Whisper WebSocket - voice transcription
whisper: buildWsUrl('/ws/whisper', 4104),
// WebMCP WebSocket
webmcp: buildWsUrl('/ws/mcp', 4102),
// API base URL (Vite proxy handles /api in dev)
api: '/api'
}
// Debug logging
console.log('[Endpoints]', isSecure ? 'HTTPS/Traefik mode' : 'Development mode', endpoints)

View File

@@ -82,7 +82,7 @@ async function connect() {
try {
// Test connection and get repo info
const res = await fetch('http://localhost:4101/api/gitea/repo', {
const res = await fetch('/api/gitea/repo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -119,7 +119,7 @@ async function connect() {
async function loadFileTree(path: string = '') {
try {
const res = await fetch('http://localhost:4101/api/gitea/tree', {
const res = await fetch('/api/gitea/tree', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -184,7 +184,7 @@ async function selectFile(node: FileNode) {
fileContent.value = ''
try {
const res = await fetch('http://localhost:4101/api/gitea/file', {
const res = await fetch('/api/gitea/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({

View File

@@ -4,6 +4,7 @@ import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { WebLinksAddon } from '@xterm/addon-web-links'
import '@xterm/xterm/css/xterm.css'
import { endpoints } from '../config/endpoints'
const terminalContainer = ref<HTMLElement | null>(null)
const connected = ref(false)
@@ -17,7 +18,7 @@ let fitAddon: FitAddon | null = null
let socket: WebSocket | null = null
let resizeObserver: ResizeObserver | null = null
const WS_URL = `ws://${window.location.hostname}:4103`
const WS_URL = endpoints.terminal
function initTerminal() {
if (!terminalContainer.value) return

View File

@@ -20,7 +20,8 @@ import { setActivePinia, type Pinia } from 'pinia'
import { useCanvasStore } from '../stores/canvas'
import { useThemeStore } from '../stores/theme'
const API_URL = 'http://localhost:4101'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_URL = ''
// Tipos
export interface VueComponentDefinition {

View File

@@ -1,6 +1,7 @@
import type { ThemeVariables, Theme, DesignTokens } from '../stores/theme'
const API_URL = 'http://localhost:4101'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_URL = ''
// =====================
// API Client

View File

@@ -1,6 +1,7 @@
import type { ToolConfig } from './index'
const API_BASE = 'http://localhost:4101'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_BASE = ''
export function createDatabaseHandlers(): ToolConfig[] {
return [

View File

@@ -234,7 +234,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
},
handler: async () => {
try {
const res = await fetch(`http://${window.location.hostname}:4100/api/whisper/status`)
const res = await fetch('/api/whisper/status')
const data = await res.json()
return `Whisper GPU Status:\n` +
` Enabled: ${data.enabled ? 'Yes' : 'No'}\n` +
@@ -257,7 +257,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
},
handler: async () => {
try {
const res = await fetch(`http://${window.location.hostname}:4100/api/whisper/toggle`, {
const res = await fetch('/api/whisper/toggle', {
method: 'POST'
})
const data = await res.json()
@@ -266,7 +266,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
return `Whisper GPU ENABLED\n` +
` Model: ${data.model}\n` +
` Device: ${data.device}\n` +
` Port: ws://localhost:${data.port}\n\n` +
` Port: ${data.port}\n\n` +
`Voice input will now use GPU-accelerated transcription.`
} else {
return `Whisper GPU DISABLED\n\n` +
@@ -287,7 +287,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
},
handler: async () => {
try {
const res = await fetch(`http://${window.location.hostname}:4100/api/whisper/start`, {
const res = await fetch('/api/whisper/start', {
method: 'POST'
})
const data = await res.json()
@@ -315,7 +315,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo
},
handler: async () => {
try {
const res = await fetch(`http://${window.location.hostname}:4100/api/whisper/stop`, {
const res = await fetch('/api/whisper/stop', {
method: 'POST'
})
const data = await res.json()

View File

@@ -1,6 +1,7 @@
import type { ToolConfig } from './index'
const API_BASE = 'http://localhost:4101'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_BASE = ''
// Store credentials in memory
let giteaCredentials: {

View File

@@ -1,10 +1,11 @@
import { useCanvasStore } from '../stores/canvas'
import { endpoints, isSecure, wsProtocol, hostname } from '../config/endpoints'
let webmcpInstance: any = null
const registeredTools = new Set<string>()
const eventUnsubscribers: Array<() => void> = []
const API_BASE = 'http://localhost:4101'
const API_BASE = endpoints.api
let tokenPollingInterval: number | null = null
export async function initWebMCP() {
@@ -286,9 +287,22 @@ export async function connectWithToken(token: string): Promise<boolean> {
// Clear the pending token from server
await clearToken()
// If behind HTTPS/Traefik, modify token to use secure WebSocket
let finalToken = token
if (isSecure) {
const parsed = parseToken(token)
if (parsed) {
// Replace ws://localhost:4102 with wss://hostname/ws/mcp
const secureServer = `${wsProtocol}//${hostname}/ws/mcp`
const modifiedToken = { server: secureServer, token: parsed.token }
finalToken = btoa(JSON.stringify(modifiedToken))
console.log('[WebMCP] Modified token for HTTPS:', secureServer)
}
}
// Connect passing the token directly
try {
await webmcpInstance.connect(token)
await webmcpInstance.connect(finalToken)
return true
} catch (e) {
console.error('[WebMCP] Failed to connect:', e)

View File

@@ -2,7 +2,8 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { ProjectCanvas, CanvasComponent, ComponentUsage } from '../types/canvas'
const API_URL = 'http://localhost:4101'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_URL = ''
export const useProjectCanvasStore = defineStore('projectCanvas', () => {
// State

View File

@@ -44,7 +44,8 @@ export interface DesignTokens {
examples: Record<string, string>
}
const API_URL = 'http://localhost:4101'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_URL = ''
// =====================
// Store