Pular para o conteúdo principal

ArboreoLab OCR Worker - macOS Desktop App

Visão Geral

Aplicativo desktop para macOS que gerencia workers Python de OCR (Vision) com conexão SSH reversa ao servidor ArboreoLab.

Versão Atual: 1.0.26

Stack Tecnológico

TecnologiaVersãoPropósito
Quasar Framework2.18.3UI Framework (Material Design)
Vue.js3.5.13Framework reativo (Composition API)
Electron39.2.7Runtime desktop cross-platform
TypeScript5.7.xTipagem estática
Pinia2.3.0State management
ssh21.17.0Conexão SSH nativa (sem dependência de CLI)
Socket.IO5.10.0WebSocket bidirecional (complementa SSH)
Vite6.4.1Build tool
electron-builder26.0.12Empacotamento

Estrutura do Projeto

macos-worker-interface/
├── src/ # Frontend Vue.js
│ ├── App.vue # Componente raiz
│ ├── css/
│ │ └── quasar.variables.scss # Variáveis de tema (dark mode)
│ ├── components/
│ │ ├── DashboardPanel.vue # Painel principal com status
│ │ ├── JobQueue.vue # Fila de jobs OCR
│ │ ├── LogViewer.vue # Visualizador de logs em tempo real
│ │ └── SettingsPanel.vue # Configurações (Token, Python, SSH)
│ ├── layouts/
│ │ └── MainLayout.vue # Layout com sidebar e header
│ ├── pages/
│ │ ├── DashboardPage.vue # Página do dashboard
│ │ ├── JobsPage.vue # Página de jobs
│ │ ├── LogsPage.vue # Página de logs
│ │ └── SettingsPage.vue # Página de configurações
│ ├── router/
│ │ └── index.ts # Vue Router configuração
│ ├── stores/
│ │ ├── app.ts # Store principal (conexão, status)
│ │ ├── logs.ts # Store centralizado de logs (singleton)
│ │ └── settings.ts # Store de configurações persistentes
│ └── types/
│ └── index.ts # Tipos TypeScript compartilhados
├── src-electron/ # Backend Electron (Node.js)
│ ├── electron-main.ts # Processo principal
│ ├── electron-preload.ts # Bridge seguro (contextBridge)
│ └── services/
│ ├── LogWatcher.ts # Monitor de logs do worker
│ ├── PythonManager.ts # Gerenciador Python/venv
│ ├── SshTunnelManager.ts # Gerenciador de túnel SSH
│ └── TokenManager.ts # Gerenciador de tokens de acesso
├── resources/
│ └── python/
│ ├── worker_server.py # Worker Python (Vision OCR)
│ ├── websocket_client.py # Cliente Socket.IO (WebSocket)
│ └── requirements.txt # Dependências Python
├── build/
│ ├── icon.png # Ícone 1024x1024
│ ├── icon.icns # Ícone macOS
│ └── icons/ # Ícones em vários tamanhos
├── quasar.config.ts # Configuração Quasar
├── electron-builder.yml # Configuração de empacotamento
├── package.json # Dependências e scripts
└── tsconfig.json # Configuração TypeScript

Arquitetura

Separação de Processos (Electron)

┌─────────────────────────────────────────────────────────────────┐
│ RENDERER PROCESS │
│ (Vue.js 3 + Quasar + Pinia) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ DashboardPanel│ │ JobQueue │ │ LogViewer │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ window.electronAPI │
└────────────────────────────┼────────────────────────────────────┘
│ IPC (contextBridge)
┌────────────────────────────┼────────────────────────────────────┐
│ MAIN PROCESS │
│ (Node.js + Electron) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ TokenManager │ │ SshTunnel │ │ PythonManager│ │
│ └──────────────┘ │ Manager │ └──────────────┘ │
│ └──────────────┘ │
│ │ │
│ ssh2 (nativo) │
└────────────────────────────┼────────────────────────────────────┘

┌────────▼────────┐
│ Servidor SSH │
│ (ArboreoLab) │
│ │
│ Reverse Tunnel │
│ 9001 → 8766 │
└─────────────────┘

Arquitetura de Comunicação (Dual-Path)

┌─────────────────┐                                   ┌─────────────────┐
│ Mac Worker │ WebSocket (Socket.IO) │ Node Backend │
│ (Python) │◄─────────────────────────────────►│ (Express) │
│ │ wss:// - bidirectional │ │
│ │ │ │
│ │ SSH Tunnel (Reverso) │ │
│ │◄─────────────────────────────────►│ │
│ │ Port 9001 → 8766 │ │
└─────────────────┘ └─────────────────┘

Redundância: WebSocket e SSH tunnel funcionam em paralelo para máxima resiliência.

Fluxo de Dados

  1. Token de Acesso: Usuário importa arquivo .arbtoken com credenciais SSH
  2. Conexão SSH: SshTunnelManager estabelece túnel reverso usando ssh2
  3. Worker Python: PythonManager gerencia venv e executa worker_server.py
  4. Comunicação: Servidor acessa worker local via porta 8766 (túnel reverso)

Serviços do Main Process

TokenManager (src-electron/services/TokenManager.ts)

Gerencia tokens de acesso ArboreoLab.

interface AccessToken {
version: string;
created: string;
project: string;
host: string;
port: number;
username: string;
privateKey: string;
remotePort: number;
localPort: number;
}

// Métodos principais
getToken(): AccessToken | null
saveToken(token: AccessToken): void
importTokenFile(filePath: string): AccessToken
clearToken(): void

Armazenamento: ~/.arboreolab/access_token.json

SshTunnelManager (src-electron/services/SshTunnelManager.ts)

Gerencia conexão SSH com túnel reverso usando biblioteca ssh2 nativa.

Features (v1.0.22+):

  • Reconexão infinita (autossh-like behavior)
  • Backoff exponencial + jitter (evita thundering herd)
  • Tunnel health check ativo (exec dummy a cada 60s)
  • Estatísticas de conexão (observability)
// Configuração do túnel
connect(config: {
host: string;
port: number;
username: string;
privateKey: string;
remotePort: number; // Porta no servidor (9001)
localPort: number; // Porta local do worker (8766)
}): Promise<void>

disconnect(): void
getStatus(): { connected: boolean; stats: ConnectionStats; uptime?: number }

Parâmetros de Resiliência:

ParâmetroValorDescrição
maxReconnectAttempts0 (infinito)Nunca desiste
reconnectDelay5000msDelay base
maxReconnectDelay60000msDelay máximo
keepaliveInterval30000msSSH keepalive
tunnelHealthInterval60000msHealth check ativo

Túnel Reverso: O servidor conecta na porta remotePort (9001) e o tráfego é redirecionado para localPort (8766) onde o worker Python escuta.

PythonManager (src-electron/services/PythonManager.ts)

Gerencia ambiente Python e worker.

Features (v1.0.20+):

  • Health check tolerante (3 falhas consecutivas antes de alertar)
  • Intervalo aumentado (60s) para não sobrecarregar durante OCR
  • Timeout aumentado (15s) para workers ocupados

Features (v1.0.24+):

  • isStarting lock para evitar múltiplas inicializações simultâneas
  • Método start() agora é async para sequenciamento correto

Features (v1.0.26+):

  • killProcessOnPort(port): Limpa processos órfãos antes de iniciar worker
  • Usa lsof -ti:${port} para encontrar PIDs ocupando a porta
  • Previne erro "address already in use" em restarts
// Verificar ambiente
checkEnvironment(): Promise<{
pythonInstalled: boolean;
pythonPath: string;
pythonVersion: string;
venvExists: boolean;
venvPath: string;
requirementsInstalled: boolean;
}>

// Configurar venv
setupVenv(onProgress?: (msg: string) => void): Promise<void>

// Remover venv
removeVenv(): Promise<void>

// Executar worker (async, com cleanup de porta)
async start(): Promise<void> // v1.0.26: async + killProcessOnPort
stopWorker(): void

// Limpar processos órfãos (v1.0.26+)
async killProcessOnPort(port: number): Promise<void>

Parâmetros de Health Check:

ParâmetroValorDescrição
HEALTH_CHECK_INTERVAL60000msIntervalo entre checks
HEALTH_CHECK_TIMEOUT15000msTimeout por check
MAX_HEALTH_FAILURES3Falhas antes de alertar

Venv: Criado em ~/.arboreolab/venv/ Worker: Executa worker_server.py na porta 8766


🎨 Padrões de Catálogo de Arte

O worker inclui padrões especializados para processamento de catálogos de arte, integrados diretamente na função _process_column_to_markdown().

Padrões Reconhecidos

TipoRegexExemploFormatação
Artista^[A-Z]{2,}(?:\s+[A-Za-z]+)+$IKEDA Masuo**negrito**
Dimensão^\d+\s*[Xx×]\s*\d+162 X 130*itálico*
Ano^(18|19|20)\d{2}$1962texto simples
TécnicaLista de palavras-chaveLitografía*itálico*
Nº Catálogo^\d{1,4}\.\s+\S33. Orbita- lista
Tiragem^\d+/\d+$ ou A.P.1/50texto simples

Prioridade de Processamento

1. CATALOG_PATTERNS (artistas, dimensões, técnicas)
↓ não match
2. Heurísticas genéricas (CAPS = título, números = lista)

Isso garante que dimensões (162 X 130) nunca sejam classificadas como títulos (## 162 X 130).

LogWatcher (src-electron/services/LogWatcher.ts)

Monitora logs do worker Python.

start(logPath: string): void
stop(): void
onLog(callback: (line: string) => void): void

🌐 WebSocket (Socket.IO) - v1.0.23+

Comunicação bidirecional em tempo real que complementa o SSH tunnel.

Arquitetura

┌─────────────────┐                          ┌─────────────────┐
│ Worker Python │ Socket.IO Client │ Node Backend │
│ websocket_ │◄────────────────────────►│ workerSocket │
│ client.py │ Auto-reconnect │ Service.js │
│ │ Heartbeat 30s │ │
└─────────────────┘ └─────────────────┘

Cliente Python (resources/python/websocket_client.py)

class WorkerWebSocketClient:
def __init__(
self,
server_url: str, # https://srv1.arboreolab.com.br
worker_name: str, # mac-vision-01
auth_token: str = None,
on_job_received: Callable = None,
on_update_available: Callable = None
)

async def connect() -> bool
async def disconnect()
async def emit_job_progress(job_id: int, progress: int, message: str)
async def emit_job_completed(job_id: int, result: dict)
async def emit_job_failed(job_id: int, error: str)

Servidor Node.js (node/backend/services/workerSocketService.js)

// Inicialização
initialize(httpServer, options)

// Enviar job para worker específico
sendJobToWorker(workerName, jobData): Promise

// Enviar job para qualquer worker disponível
sendJobToAnyWorker(jobData): Promise

// Status de todos workers
getWorkersStatus(): { total, idle, processing, workers[] }

// Notificar update
notifyWorkersOfUpdate(updateInfo)

Endpoints REST (WebSocket)

EndpointMétodoDescrição
/api/workers/websocket/statusGETStatus de workers conectados
/api/workers/websocket/jobPOSTEnviar job via WebSocket
/api/workers/websocket/notify-updatePOSTNotificar update para todos

Eventos Socket.IO

EventoDireçãoPayload
heartbeatWorker → Server{ status, currentJob, timestamp }
job:processServer → Worker{ jobId, fileIds, callbackUrl, ... }
job:progressWorker → Server{ jobId, progress, message }
job:completedWorker → Server{ jobId, result }
job:failedWorker → Server{ jobId, error }
update:availableServer → Worker{ version, downloadUrl, changelog }

Configuração

# Habilitar/desabilitar WebSocket no worker
WEBSOCKET_ENABLED=true # default

# Dependência Python
pip install python-socketio[asyncio_client]

🐍 Worker Python (resources/python/worker_server.py)

Servidor FastAPI que processa OCR usando Apple Vision Framework.

Configuração

VariávelDefaultDescrição
WORKER_NAMEmac-vision-01Nome único do worker
LINUX_SERVER_URLhttps://srv1.arboreolab.com.brURL do servidor
CALLBACK_TIMEOUT180Timeout base para callbacks (segundos)
WEBSOCKET_ENABLEDtrueHabilitar WebSocket
OCR_TEMP_DIR/tmp/ocr_jobsDiretório temporário

Callback com Retry (v1.0.21+)

O worker implementa retry automático para callbacks:

# Configuração de retry
MAX_RETRIES = 3
BACKOFF_DELAYS = [10, 20, 30] # segundos entre tentativas
TIMEOUT_PROGRESSION = [180, 240, 300] # timeout crescente
TentativaWait BeforeTimeoutTotal Elapsed
10s180s180s
210s240s430s
320s300s750s

Importante: O job é marcado como sucesso se o OCR completou, mesmo que o callback falhe após todas as tentativas.

Endpoints

EndpointMétodoDescrição
/healthGETHealth check do worker
/processPOSTIniciar job OCR
/jobs/{id}GETStatus de job específico
/update/notifyPOSTReceber notificação de update

Progress Reporting

O worker reporta progresso via HTTP e/ou WebSocket:

async def report_progress(job_id: str, progress: int, message: str):
# Tenta WebSocket primeiro (mais eficiente)
if ws_client and ws_client.is_connected:
await ws_client.emit_job_progress(...)
return

# Fallback para HTTP
async with httpx.AsyncClient() as client:
await client.post(f"{SERVER}/api/workers/jobs/{job_id}/progress", ...)

Frontend (Vue.js 3)

Stores (Pinia)

AppStore (src/stores/app.ts)

interface AppState {
connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error';
workerStatus: 'stopped' | 'starting' | 'running' | 'error';
pythonStatus: PythonEnvironment | null;
logs: LogEntry[];
jobs: Job[];
}

SettingsStore (src/stores/settings.ts)

interface Settings {
token: AccessToken | null;
autoConnect: boolean;
autoStartWorker: boolean;
theme: 'dark' | 'light';
}

LogsStore (src/stores/logs.ts) - v1.0.25+

Store centralizado para gerenciamento de logs com padrão singleton global.

Problema resolvido: Logs duplicados 18-60x devido a múltiplos listeners IPC registrados em hot-reload.

Solução: Flags globais fora do Pinia store (variáveis no nível do módulo).

// ⚠️ CRÍTICO: Flags FORA do defineStore para persistir entre re-instanciações
let globalListenersInitialized = false;
let globalListenerRegistrations: (() => void)[] = [];

interface LogsState {
logs: LogEntry[];
maxLogs: number;
}

// Métodos principais
initListeners(): void // Registra IPC listeners (apenas 1x)
addLog(log: LogEntry): void
clearLogs(): void

Uso correto:

// MainLayout.vue - inicializa UMA vez no app
import { useLogsStore } from '@/stores/logs';

const logsStore = useLogsStore();
logsStore.initListeners(); // Safe: só executa se !globalListenersInitialized

Por que funciona:

  • globalListenersInitialized está no escopo do módulo ES
  • Módulos são singletons em JavaScript (cached após primeiro import)
  • Mesmo que Pinia re-instancie a store, a flag permanece true

Componentes Principais

SettingsPanel.vue

Configurações divididas em seções expansíveis:

  • Token de Acesso: Importar/visualizar token
  • Ambiente Python: Status do venv, botão de setup
  • Conexão SSH: Status e controle do túnel
  • Worker Python: Status e controle do processo

DashboardPanel.vue

Visão geral com cards de status:

  • Status da conexão SSH
  • Status do worker Python
  • Jobs processados
  • Última atividade

LogViewer.vue

Terminal virtual com logs em tempo real:

  • Auto-scroll
  • Colorização por nível (info, warn, error)
  • Filtros

IPC (Inter-Process Communication)

Preload API (electron-preload.ts)

// Exposto via contextBridge como window.electronAPI
interface ElectronAPI {
// Token
getToken(): Promise<AccessToken | null>;
importToken(filePath: string): Promise<AccessToken>;
clearToken(): Promise<void>;

// SSH
connectSsh(config: SshConfig): Promise<void>;
disconnectSsh(): Promise<void>;
getSshStatus(): Promise<ConnectionStatus>;
onSshStatus(callback: (status: string) => void): void;

// Python
checkPythonEnvironment(): Promise<PythonEnvironment>;
setupPythonVenv(): Promise<void>;
removePythonVenv(): Promise<void>;
onPythonProgress(callback: (msg: string) => void): void;

// Worker
startWorker(): Promise<void>;
stopWorker(): Promise<void>;
getWorkerStatus(): Promise<WorkerStatus>;

// Logs
onLog(callback: (log: LogEntry) => void): void;

// Dialog
showOpenDialog(options: OpenDialogOptions): Promise<string[]>;
}

Handlers no Main Process

// Token handlers
ipcMain.handle('token:get', () => tokenManager.getToken());
ipcMain.handle('token:import', (_, path) => tokenManager.importTokenFile(path));
ipcMain.handle('token:clear', () => tokenManager.clearToken());

// SSH handlers
ipcMain.handle('ssh:connect', (_, config) => sshManager.connect(config));
ipcMain.handle('ssh:disconnect', () => sshManager.disconnect());
ipcMain.handle('ssh:status', () => sshManager.getStatus());

// Python handlers
ipcMain.handle('python:check', () => pythonManager.checkEnvironment());
ipcMain.handle('python:setup', () => pythonManager.setupVenv());
ipcMain.handle('python:remove', () => pythonManager.removeVenv());

Configuração Quasar (quasar.config.ts)

export default configure(() => ({
build: {
target: { browser: ['chrome130'] },
vueRouterMode: 'hash',
},

framework: {
plugins: ['Notify', 'Dialog', 'Loading'],
iconSet: 'material-icons',
},

electron: {
bundler: 'builder',
builder: {
appId: 'com.arboreolab.ocrworker',
productName: 'ArboreoLab OCR Worker',
mac: {
identity: null, // Sem assinatura
target: ['dir', 'zip'],
},
},
},
}));

Build e Distribuição

Scripts npm

# Desenvolvimento
npm run dev # Inicia em modo dev com hot-reload

# Build
npm run build # Build Quasar + Electron

# Empacotamento macOS (Apple Silicon)
npx electron-builder --mac --arm64 --config electron-builder.yml

electron-builder.yml

appId: com.arboreolab.ocrworker
productName: ArboreoLab OCR Worker
electronVersion: "39.2.7"

directories:
output: dist/electron/Packaged
buildResources: build
app: dist/electron/UnPackaged

mac:
category: public.app-category.productivity
icon: build/icon.png
identity: null # Sem assinatura
hardenedRuntime: false
gatekeeperAssess: false
target:
- target: dir
arch: [arm64] # Apple Silicon only
- target: zip
arch: [arm64]

extraResources:
- from: "resources/python"
to: "python"

Sistema de Auto-Update

O app possui sistema de atualização automática que:

  1. Recebe notificação do servidor via endpoint /update/notify
  2. Baixa o tarball fonte do servidor
  3. Extrai e recompila localmente no Mac
  4. Substitui o app atual
# Notificar workers sobre nova versão
curl -X POST http://localhost:9001/update/notify \
-H "Content-Type: application/json" \
-d '{
"version": "1.0.14",
"changelog": "Descrição da atualização",
"downloadUrl": "https://srv1.arboreolab.com.br/downloads/macos-worker-v1.0.14-src.tar.gz",
"size": 135029
}'

Build Local no macOS (Recomendado)

O build nativo no macOS é necessário para Apple Silicon:

# No Mac (Apple Silicon)
cd macos-worker-interface
npm install
npm run build
npx electron-builder --mac --arm64 --config electron-builder.yml

# Output: dist/electron/Packaged/ArboreoLab OCR Worker-1.0.14-arm64-mac.zip

Importante: Intel (x64) não é mais suportado. Apenas Apple Silicon (arm64).


Instalação no macOS (Usuário Final)

Sem Assinatura Apple

O app não possui assinatura, então o macOS bloqueará na primeira execução:

# Remover quarentena
xattr -cr '/Applications/ArboreoLab OCR Worker.app'

Ou via Preferências do Sistema → Privacidade e Segurança → "Abrir Mesmo Assim"

Requisitos

  • macOS 11.0+ (Big Sur ou superior) - Apple Silicon (M1/M2/M3/M4)
  • Python 3.8+ instalado (para o worker)
  • Node.js 18+ e npm (para auto-update/recompilação)
  • Conexão com internet (para SSH e updates)

Padrões de Código

TypeScript Strict

// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

Vue 3 Composition API

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useAppStore } from '@/stores/app';

const appStore = useAppStore();
const isConnected = computed(() => appStore.connectionStatus === 'connected');

onMounted(async () => {
await appStore.initialize();
});
</script>

Tratamento de Erros

// Main process - sempre try/catch
ipcMain.handle('ssh:connect', async (_, config) => {
try {
await sshManager.connect(config);
return { success: true };
} catch (error) {
console.error('SSH connection failed:', error);
return { success: false, error: (error as Error).message };
}
});

// Renderer - usar composable useToast
const { showError } = useToast();
try {
await window.electronAPI.connectSsh(config);
} catch (error) {
showError('Falha na conexão SSH');
}

Troubleshooting

Erro: "Object has been destroyed" (Electron crash)

Causa: Chamadas IPC para mainWindow após a janela ser destruída (fechar app durante evento SSH).

Solução (v1.0.18+): Usar safeSend() pattern em todos os services:

private isWindowAvailable(): boolean {
return !!(this.mainWindow && !this.mainWindow.isDestroyed());
}

private safeSend(channel: string, data: unknown): void {
if (this.isWindowAvailable()) {
try {
this.mainWindow!.webContents.send(channel, data);
} catch (e) {
console.warn(`Janela destruída: ${channel}`);
}
}
}

Erro: "Not allowed to load local resource"

Causa: Caminho do index.html incorreto no electron-main.ts

Solução: Verificar mainWindow.loadFile() usa caminho correto:

// Em produção, index.html está na raiz do app
mainWindow.loadFile(path.join(__dirname, 'index.html'));

Erro: httpx.ReadTimeout no callback

Causa: Timeout muito curto para payloads grandes via SSH tunnel.

Solução (v1.0.21+): Callback retry automático com timeout progressivo:

# Configurado no worker_server.py
CALLBACK_TIMEOUT = 180 # Base
# Retries: 180s → 240s → 300s

Erro: Logs duplicados 18-60x (v1.0.26 fix)

Causa: Múltiplos listeners IPC registrados durante hot-reload ou re-renderização de componentes.

Sintomas:

  • Cada log aparece 18-60 vezes no LogViewer
  • Worker reinicia em loop infinito
  • Mensagens "address already in use" para porta 8766

Solução (v1.0.25-26):

  1. Store centralizado: src/stores/logs.ts com flags globais
  2. Flags fora do Pinia: Variáveis no nível do módulo ES
  3. Inicialização única: initListeners() só executa 1x
// src/stores/logs.ts
let globalListenersInitialized = false; // FORA do defineStore!

export const useLogsStore = defineStore('logs', () => {
function initListeners() {
if (globalListenersInitialized) return; // Previne duplicação
globalListenersInitialized = true;

window.electronAPI.onLog((log) => {
addLog(log);
});
}
});

Erro: "address already in use" porta 8766 (v1.0.26 fix)

Causa: Processos Python órfãos ocupando a porta após crash/restart.

Solução (v1.0.26): killProcessOnPort() no PythonManager:

// src-electron/services/PythonManager.ts
async function killProcessOnPort(port: number): Promise<void> {
const { exec } = require('child_process');
exec(`lsof -ti:${port} | xargs kill -9`, (error) => {
// Ignora erros se não houver processo
});
}

// Chamado antes de iniciar worker
async start(): Promise<void> {
await killProcessOnPort(8766); // Limpa órfãos
// ... spawn worker
}

Erro no Auto-Update

Causa: URL de download incorreta ou arquivo não encontrado

Solução: Verificar se o tarball está no servidor correto:

# URL correta
https://srv1.arboreolab.com.br/downloads/macos-worker-vX.X.X-src.tar.gz

# NÃO usar geopoliticas.com (não tem /downloads configurado)

SSH: "object could not be cloned"

Causa: Objeto reativo Vue sendo passado para IPC

Solução: Converter para objeto plano:

const config = JSON.parse(JSON.stringify(toRaw(reactiveConfig)));
await window.electronAPI.connectSsh(config);

SSH: Host verification failed

Causa: Chave do host desconhecida

Solução: No SshTunnelManager:

hostVerifier: () => true  // Aceitar qualquer host (dev/produção controlada)

Referências


📋 Histórico de Versões

VersãoDataPrincipais Mudanças
1.0.262026-01-06Fix definitivo log duplication (global singleton fora Pinia), killProcessOnPort() para limpar processos órfãos
1.0.252026-01-06Centralized logs store (src/stores/logs.ts), refatoração LogViewer/DashboardPage
1.0.242026-01-06Fix worker restart loop, isStarting lock no PythonManager
1.0.232026-01-06WebSocket (Socket.IO) bidirecional, dual-path communication
1.0.222026-01-06Reconexão infinita SSH (autossh-like), backoff + jitter, tunnel health check
1.0.212026-01-06Callback retry (3 tentativas, backoff exponencial)
1.0.202026-01-06Health check tolerante (3 falhas antes de alertar)
1.0.192026-01-06Progress indicator no frontend
1.0.182026-01-06Fix crash "Object has been destroyed", CALLBACK_TIMEOUT 180s