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:
@@ -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')
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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[]>([])
|
||||
|
||||
@@ -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('')
|
||||
|
||||
44
frontend/src/config/endpoints.ts
Normal file
44
frontend/src/config/endpoints.ts
Normal 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)
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user