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

@@ -1,4 +1,131 @@
{ {
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"processing\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"PreToolUse": [
{
"matcher": "Read|Glob|Grep",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"reading\\\",\\\"tool\\\":\\\"$CLAUDE_TOOL_NAME\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"writing\\\",\\\"tool\\\":\\\"$CLAUDE_TOOL_NAME\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
},
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"toolUse\\\",\\\"tool\\\":\\\"$CLAUDE_TOOL_NAME\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"toolDone\\\",\\\"tool\\\":\\\"$CLAUDE_TOOL_NAME\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"sessionStart\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"SubagentStart": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"subagentStart\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"SubagentStop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"subagentStop\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"PermissionRequest": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"permissionRequest\\\",\\\"tool\\\":\\\"$CLAUDE_TOOL_NAME\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"Notification": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"notification\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"idle\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"",
"timeout": 5000
}
]
}
]
},
"permissions": { "permissions": {
"allow": [ "allow": [
"Skill(playwright-cli)", "Skill(playwright-cli)",

View File

@@ -11,6 +11,7 @@ import FloatingResponse from './components/FloatingResponse.vue'
import FloatingVoice from './components/FloatingVoice.vue' import FloatingVoice from './components/FloatingVoice.vue'
import PwaInstallBanner from './components/PwaInstallBanner.vue' import PwaInstallBanner from './components/PwaInstallBanner.vue'
import { initWebMCP, getWebMCP, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp' import { initWebMCP, getWebMCP, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp'
import { endpoints } from './config/endpoints'
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry' import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
import { setTerminalControls } from './services/tools/handlers/terminalHandlers' import { setTerminalControls } from './services/tools/handlers/terminalHandlers'
import { setResponseControls } from './services/tools/handlers/responseHandlers' import { setResponseControls } from './services/tools/handlers/responseHandlers'
@@ -48,7 +49,7 @@ let toolFlashTimeout: number | null = null
function connectStatusWs() { function connectStatusWs() {
if (statusWs?.readyState === WebSocket.OPEN) return if (statusWs?.readyState === WebSocket.OPEN) return
statusWs = new WebSocket(`ws://${window.location.hostname}:4103`) statusWs = new WebSocket(endpoints.claudeStatus)
statusWs.onopen = () => { statusWs.onopen = () => {
console.log('[App] Status WebSocket connected') 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 '@xterm/xterm/css/xterm.css'
import { connectWithToken, stopTokenPolling } from '../services/webmcp' import { connectWithToken, stopTokenPolling } from '../services/webmcp'
import { useCanvasStore } from '../stores/canvas' import { useCanvasStore } from '../stores/canvas'
import { endpoints } from '../config/endpoints'
const props = defineProps<{ const props = defineProps<{
modelValue: boolean modelValue: boolean
@@ -47,7 +48,7 @@ let tokenBuffer = ''
let tokenTimeout: number | null = null let tokenTimeout: number | null = null
const waitingForToken = ref(false) const waitingForToken = ref(false)
const WS_URL = `ws://${window.location.hostname}:4103` const WS_URL = endpoints.terminal
// Mouse position tracking for Ctrl+E // Mouse position tracking for Ctrl+E
const mousePos = ref({ x: 0, y: 0 }) const mousePos = ref({ x: 0, y: 0 })

View File

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

View File

@@ -1,7 +1,8 @@
import { ref } from 'vue' import { ref } from 'vue'
import type { TableInfo, TableSchema, DbStats } from '@/types/database' 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() { export function useDatabaseApi() {
const tables = ref<TableInfo[]>([]) const tables = ref<TableInfo[]>([])

View File

@@ -1,6 +1,7 @@
import { ref } from 'vue' 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() { export function useQueryExecutor() {
const queryText = ref('') 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 { try {
// Test connection and get repo info // 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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -119,7 +119,7 @@ async function connect() {
async function loadFileTree(path: string = '') { async function loadFileTree(path: string = '') {
try { try {
const res = await fetch('http://localhost:4101/api/gitea/tree', { const res = await fetch('/api/gitea/tree', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -184,7 +184,7 @@ async function selectFile(node: FileNode) {
fileContent.value = '' fileContent.value = ''
try { try {
const res = await fetch('http://localhost:4101/api/gitea/file', { const res = await fetch('/api/gitea/file', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({

View File

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

View File

@@ -20,7 +20,8 @@ import { setActivePinia, type Pinia } from 'pinia'
import { useCanvasStore } from '../stores/canvas' import { useCanvasStore } from '../stores/canvas'
import { useThemeStore } from '../stores/theme' 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 // Tipos
export interface VueComponentDefinition { export interface VueComponentDefinition {

View File

@@ -1,6 +1,7 @@
import type { ThemeVariables, Theme, DesignTokens } from '../stores/theme' 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 // API Client

View File

@@ -1,6 +1,7 @@
import type { ToolConfig } from './index' 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[] { export function createDatabaseHandlers(): ToolConfig[] {
return [ return [

View File

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

View File

@@ -1,6 +1,7 @@
import type { ToolConfig } from './index' 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 // Store credentials in memory
let giteaCredentials: { let giteaCredentials: {

View File

@@ -1,10 +1,11 @@
import { useCanvasStore } from '../stores/canvas' import { useCanvasStore } from '../stores/canvas'
import { endpoints, isSecure, wsProtocol, hostname } from '../config/endpoints'
let webmcpInstance: any = null let webmcpInstance: any = null
const registeredTools = new Set<string>() const registeredTools = new Set<string>()
const eventUnsubscribers: Array<() => void> = [] const eventUnsubscribers: Array<() => void> = []
const API_BASE = 'http://localhost:4101' const API_BASE = endpoints.api
let tokenPollingInterval: number | null = null let tokenPollingInterval: number | null = null
export async function initWebMCP() { export async function initWebMCP() {
@@ -286,9 +287,22 @@ export async function connectWithToken(token: string): Promise<boolean> {
// Clear the pending token from server // Clear the pending token from server
await clearToken() 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 // Connect passing the token directly
try { try {
await webmcpInstance.connect(token) await webmcpInstance.connect(finalToken)
return true return true
} catch (e) { } catch (e) {
console.error('[WebMCP] Failed to connect:', e) console.error('[WebMCP] Failed to connect:', e)

View File

@@ -2,7 +2,8 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import type { ProjectCanvas, CanvasComponent, ComponentUsage } from '../types/canvas' 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', () => { export const useProjectCanvasStore = defineStore('projectCanvas', () => {
// State // State

View File

@@ -44,7 +44,8 @@ export interface DesignTokens {
examples: Record<string, string> 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 // Store

248
traefik/agent-ui.yml Normal file
View File

@@ -0,0 +1,248 @@
## ============================================
## Agent UI - Traefik Dynamic Configuration
## ============================================
##
## INSTALACIÓN:
## 1. Copia este archivo a tu directorio de config dinámica de Traefik
## Ejemplo: /etc/traefik/dynamic/agent-ui.yml
##
## 2. Asegúrate de tener el file provider habilitado en traefik.yml:
## providers:
## file:
## directory: /etc/traefik/dynamic
## watch: true
##
## 3. Asegúrate de tener el certResolver configurado (letsencrypt)
##
## ARQUITECTURA:
## ┌─────────────────────────────────────────────────────┐
## │ https://z590.nucleoriofrio.com │
## │ / → Frontend (4100) │
## │ /api/* → API Server (4101) │
## │ /ws/terminal→ Terminal WebSocket (4103) │
## │ /ws/status → Claude Status WebSocket (4103) │
## │ /ws/mcp → WebMCP WebSocket (4102) │
## │ /ws/whisper → Whisper WebSocket (4104) │
## └─────────────────────────────────────────────────────┘
##
http:
## ============================================
## MIDDLEWARES
## ============================================
middlewares:
## ----------------------------------------
## IP Whitelist (DESCOMENTEAR PARA ACTIVAR)
## ----------------------------------------
## Para activar: descomenta este bloque y agrega
## "agentui-ipwhitelist" a la lista de middlewares
## de cada router.
##
# agentui-ipwhitelist:
# ipWhiteList:
# sourceRange:
# - "192.168.87.0/24" # Red local del servidor
# - "192.168.1.0/24" # Otra red local
# - "10.0.0.0/8" # Redes internas
# - "YOUR_PUBLIC_IP/32" # Tu IP pública específica
# # Si hay proxies intermedios, usa ipStrategy:
# # ipStrategy:
# # depth: 1
## ----------------------------------------
## Strip Prefix para WebSockets
## ----------------------------------------
## Traefik recibe /ws/terminal pero el backend espera /
strip-ws-terminal:
stripPrefix:
prefixes:
- "/ws/terminal"
strip-ws-status:
stripPrefix:
prefixes:
- "/ws/status"
strip-ws-mcp:
stripPrefix:
prefixes:
- "/ws/mcp"
strip-ws-whisper:
stripPrefix:
prefixes:
- "/ws/whisper"
## ----------------------------------------
## Headers de seguridad
## ----------------------------------------
agentui-headers:
headers:
customRequestHeaders:
X-Forwarded-Proto: "https"
# Headers de seguridad opcionales:
# stsSeconds: 31536000
# stsIncludeSubdomains: true
# contentTypeNosniff: true
# browserXssFilter: true
## ============================================
## ROUTERS
## ============================================
routers:
## ----------------------------------------
## Frontend (/) - Vue App
## ----------------------------------------
## Sirve la aplicación Vue, assets, PWA manifest, etc.
## Excluye /api y /ws para que los otros routers los manejen.
##
agentui-frontend:
rule: "Host(`z590.nucleoriofrio.com`) && !PathPrefix(`/api`) && !PathPrefix(`/ws`)"
entryPoints:
- websecure
service: agentui-frontend
middlewares:
- agentui-headers
# - agentui-ipwhitelist # Descomentar para activar
tls:
certResolver: letsencrypt
priority: 1
## ----------------------------------------
## API Backend (/api/*) - REST API
## ----------------------------------------
## Maneja todas las llamadas REST:
## - /api/health - Health check
## - /api/themes/* - Sistema de temas
## - /api/components/* - Componentes Vue guardados
## - /api/database/* - Explorador de base de datos
## - /api/claude-status - Status de Claude (desde hooks)
## - /api/whisper/* - Control de Whisper
## - etc.
##
agentui-api:
rule: "Host(`z590.nucleoriofrio.com`) && PathPrefix(`/api`)"
entryPoints:
- websecure
service: agentui-api
middlewares:
- agentui-headers
# - agentui-ipwhitelist
tls:
certResolver: letsencrypt
priority: 10
## ----------------------------------------
## Terminal WebSocket (/ws/terminal)
## ----------------------------------------
## Conexión WebSocket para xterm.js
## Permite ejecutar comandos en el servidor.
##
agentui-ws-terminal:
rule: "Host(`z590.nucleoriofrio.com`) && PathPrefix(`/ws/terminal`)"
entryPoints:
- websecure
service: agentui-terminal
middlewares:
- strip-ws-terminal
# - agentui-ipwhitelist
tls:
certResolver: letsencrypt
priority: 20
## ----------------------------------------
## Claude Status WebSocket (/ws/status)
## ----------------------------------------
## Recibe actualizaciones de estado de Claude/Nucleo
## para las animaciones del FAB.
## Usa el mismo backend que terminal (4103).
##
agentui-ws-status:
rule: "Host(`z590.nucleoriofrio.com`) && PathPrefix(`/ws/status`)"
entryPoints:
- websecure
service: agentui-terminal
middlewares:
- strip-ws-status
# - agentui-ipwhitelist
tls:
certResolver: letsencrypt
priority: 20
## ----------------------------------------
## WebMCP WebSocket (/ws/mcp)
## ----------------------------------------
## Bridge entre Claude Code MCP y el browser.
## Permite que Claude ejecute herramientas en la UI:
## - render_vue_component
## - navigate_to
## - set_theme_variable
## - etc.
##
agentui-ws-mcp:
rule: "Host(`z590.nucleoriofrio.com`) && PathPrefix(`/ws/mcp`)"
entryPoints:
- websecure
service: agentui-mcp
middlewares:
- strip-ws-mcp
# - agentui-ipwhitelist
tls:
certResolver: letsencrypt
priority: 20
## ----------------------------------------
## Whisper WebSocket (/ws/whisper)
## ----------------------------------------
## Transcripción de voz con Whisper.
## Opcional - solo si usas la función de voz.
##
agentui-ws-whisper:
rule: "Host(`z590.nucleoriofrio.com`) && PathPrefix(`/ws/whisper`)"
entryPoints:
- websecure
service: agentui-whisper
middlewares:
- strip-ws-whisper
# - agentui-ipwhitelist
tls:
certResolver: letsencrypt
priority: 20
## ============================================
## SERVICES (Backends)
## ============================================
## Cambia 192.168.87.135 por la IP de tu servidor
## si es diferente.
##
services:
## Frontend - Vite dev server o build estático
agentui-frontend:
loadBalancer:
servers:
- url: "http://192.168.87.135:4100"
## API Server - Bun HTTP
agentui-api:
loadBalancer:
servers:
- url: "http://192.168.87.135:4101"
## Terminal WebSocket Server
## También maneja Claude status broadcast
agentui-terminal:
loadBalancer:
servers:
- url: "http://192.168.87.135:4103"
## WebMCP WebSocket Server
agentui-mcp:
loadBalancer:
servers:
- url: "http://192.168.87.135:4102"
## Whisper WebSocket Server (opcional)
agentui-whisper:
loadBalancer:
servers:
- url: "http://192.168.87.135:4104"