From 96222aa6a2cb44014a400b3b72628cd2dbc97d64 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 8 Feb 2026 13:20:15 -0500 Subject: [PATCH] chore: adiciona Docker, scripts e documentacao MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona Dockerfile e docker-compose para containerizacao - Adiciona docker-entrypoint.sh com inicializacao - Adiciona scripts/seed-admin.js para criar admin inicial - Adiciona docs/ com logos originais CambioReal - Atualiza README.md com instrucoes de uso - Atualiza queries.js com metricas de portfólio Co-Authored-By: Claude Opus 4.5 --- .dockerignore | 34 ++++ .env.example | 7 +- Dockerfile | 29 +++ README.md | 264 +++++++++++++++++++------- docker-entrypoint.sh | 32 ++++ docs/API.md | 350 +++++++++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 349 ++++++++++++++++++++++++++++++++++ docs/CambioReal_original.png | Bin 0 -> 22387 bytes docs/GUIA-USUARIO.md | 272 +++++++++++++++++++++++++++ docs/logo-small.png | Bin 0 -> 4914 bytes scripts/seed-admin.js | 70 +++++++ src/queries.js | 131 ++++++++++++- 12 files changed, 1470 insertions(+), 68 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-entrypoint.sh create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CambioReal_original.png create mode 100644 docs/GUIA-USUARIO.md create mode 100644 docs/logo-small.png create mode 100644 scripts/seed-admin.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6a43c80 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules + +# Data files (mounted as volume) +data/ + +# Environment files +.env +.env.local + +# Git +.git +.gitignore + +# Docs +docs/ +README.md +*.md + +# IDE +.vscode +.idea + +# BMAD +_bmad/ +_bmad-output/ + +# Logs +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db diff --git a/.env.example b/.env.example index f590bbc..43470a3 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ -# RDS Read-Only +# Netbird VPN (required for RDS access) +NETBIRD_SETUP_KEY=14A782C8-24D2-46A9-B427-A422854E9B50 + +# RDS Read-Only (accessible via Netbird) MYSQL_URL=seu-host-rds.region.rds.amazonaws.com USER_MYSQL=usuario_readonly PW_MYSQL=senha_aqui -# Sessão +# Sessao SESSION_SECRET=trocar-por-chave-secreta-aleatoria PORT=3080 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c94890 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# BI Agentes - Dockerfile +FROM node:20-alpine + +# Install netbird client +RUN apk add --no-cache curl bash iptables ip6tables \ + && curl -fsSL https://pkgs.netbird.io/install.sh | sh + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy application files +COPY . . + +# Create data directory for SQLite +RUN mkdir -p /app/data + +# Expose port +EXPOSE 3080 + +# Start script will handle netbird connection +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/README.md b/README.md index ce305cd..328aa3c 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,243 @@ -# BI Agentes — CambioReal +# BI Agentes - CambioReal -Dashboard de transações para agentes CambioReal com autenticação local e dados ao vivo do RDS. +Dashboard de Business Intelligence para agentes de cambio CambioReal. Monitoramento em tempo real de transacoes BRL-USD com dados do AWS RDS. + +## Visao Geral + +Sistema web que permite aos agentes de cambio visualizar suas transacoes, KPIs de performance e graficos analiticos. Cada agente ve apenas suas proprias transacoes, filtradas pelo `agente_id`. + +**Principais recursos:** +- Autenticacao segura (bcrypt + sessoes) +- KPIs em tempo real (volume, spread, IOF, ticket medio) +- Graficos interativos (Chart.js) +- Filtros por periodo, fluxo e cliente +- Exportacao de dados (em breve) ## Arquitetura ``` -┌─────────────────────────────────┐ -│ BI Agentes (Express) │ -│ │ -│ SQLite (auth) Express/HTTP │ -│ agentes.db :3080 │ -│ email/senha /login │ -│ agente_id /dashboard │ -└───────────────────────┬─────────┘ - │ READ-ONLY - ┌──────▼──────┐ - │ cambio_db │ - │ (AWS RDS) │ - └─────────────┘ +┌─────────────────────────────────────────────────────────────┐ +│ BI Agentes (Express) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Login │───▶│ Session │───▶│ Dashboard │ │ +│ │ (HTML) │ │ (Memory) │ │ (HTML) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Express Server │ │ +│ │ PORT 3080 │ │ +│ └────────────────┬─────────────────────┬───────────────┘ │ +│ │ │ │ +└────────────────────┼─────────────────────┼───────────────────┘ + │ │ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ SQLite │ │ AWS RDS │ + │ (agentes) │ │ (cambio_db) │ + │ AUTH │ │ READ-ONLY │ + └──────────────┘ └──────────────┘ ``` -- **SQLite local** — controle de agentes (email, senha bcrypt, agente_id). Sem dependência do RDS para autenticação. -- **MySQL RDS** — somente leitura. Consulta `br_transaction_to_usa` (BRL→USD) e `pagamento_br` (USD→BRL) filtradas pelo `agente_id` da sessão. +### Bancos de Dados -## Estrutura +| Banco | Tipo | Proposito | +|-------|------|-----------| +| SQLite (`agentes.db`) | Local | Autenticacao - email, senha hash, agente_id | +| MySQL (`cambio_db`) | AWS RDS | Transacoes - somente leitura | + +### Fluxos de Transacao + +| Fluxo | Tabela RDS | Descricao | +|-------|------------|-----------| +| BRL → USD | `br_transaction_to_usa` | Envio de reais para dolares | +| USD → BRL | `pagamento_br` | Recebimento de dolares em reais | + +## Estrutura do Projeto ``` bi-agentes/ -├── server.js ← Entry point (Express + sessions) +├── server.js # Entry point Express +├── package.json # Dependencias +├── .env.example # Template de configuracao +├── .gitignore +│ ├── src/ -│ ├── auth.js ← Login/logout, bcrypt, middleware requireAuth -│ ├── db-local.js ← SQLite (better-sqlite3) — tabela agentes -│ ├── db-rds.js ← MySQL pool read-only (mysql2) -│ ├── queries.js ← SQL parametrizado por agente_id -│ └── dashboard.js ← Gerador HTML (KPIs, Chart.js, tabela) +│ ├── auth.js # Login, logout, bcrypt, middleware +│ ├── db-local.js # SQLite - tabela agentes +│ ├── db-rds.js # MySQL pool read-only +│ ├── queries.js # SQL parametrizado + serializacao +│ └── dashboard.js # Geracao HTML (KPIs, graficos, tabela) +│ ├── public/ -│ └── login.html ← Tela de login +│ └── login.html # Tela de login +│ ├── scripts/ -│ └── seed-agente.js ← CLI para cadastrar/listar agentes +│ └── seed-agente.js # CLI gerenciamento de agentes +│ ├── data/ -│ └── agentes.db ← SQLite database (gerado automaticamente) -├── .env ← Credenciais RDS + SESSION_SECRET -└── .gitignore +│ └── agentes.db # SQLite (criado automaticamente) +│ +└── docs/ # Documentacao adicional + ├── ARCHITECTURE.md # Detalhes tecnicos + ├── API.md # Endpoints + └── GUIA-USUARIO.md # Manual do agente ``` -## Setup +## Instalacao + +### Pre-requisitos + +- Node.js 18+ +- Acesso ao AWS RDS (credenciais read-only) + +### Setup ```bash +# Clonar e instalar +git clone +cd bi-agentes npm install -cp .env.example .env # editar com credenciais do RDS + +# Configurar ambiente +cp .env.example .env +# Editar .env com credenciais ``` -## Configuração (.env) +### Configuracao (.env) -``` -MYSQL_URL= -USER_MYSQL= -PW_MYSQL= -SESSION_SECRET= +```env +# Conexao AWS RDS +MYSQL_URL=cambio-db.xxx.us-east-1.rds.amazonaws.com +USER_MYSQL=bi_readonly +PW_MYSQL=senha_segura + +# Aplicacao +SESSION_SECRET=chave_secreta_aleatoria_32_chars PORT=3080 ``` -## Gerenciar Agentes +## Uso -Cadastrar: +### Iniciar o Servidor ```bash -node scripts/seed-agente.js --email agente@email.com --senha 123456 --agente 76 --nome "ValorFx" +# Desenvolvimento +node server.js + +# Producao (PM2) +pm2 start server.js --name bi-agentes +pm2 save ``` -Listar: +Acesse: `http://localhost:3080` +### Gerenciar Agentes + +**Cadastrar novo agente:** +```bash +node scripts/seed-agente.js \ + --email agente@cambioreal.com \ + --senha senha123 \ + --agente 76 \ + --nome "ValorFx" +``` + +**Listar agentes:** ```bash node scripts/seed-agente.js --list ``` -## Rodar - -```bash -node server.js -# BI Agentes rodando: http://localhost:3080 +**Saida:** +``` +ID | Agente | Nome | Email | Ativo | Criado +1 | 76 | ValorFx | agente@cambioreal.com | Sim | 2024-01-15 ``` -Para produção (intranet): +## Funcionalidades -```bash -pm2 start server.js --name bi-agentes +### Dashboard + +#### KPIs Exibidos +| KPI | Descricao | +|-----|-----------| +| Transacoes | Quantidade total no periodo | +| Volume BRL | Soma em reais | +| Volume USD | Soma em dolares | +| Taxa Media | Media ponderada da taxa cobrada | +| Spread Medio | % medio sobre PTAX | +| IOF Total | Soma do IOF cobrado | +| Ticket Medio | USD medio por transacao | +| Clientes Ativos | Quantidade de clientes unicos | + +#### Graficos (Chart.js) +- **Volume por Periodo** - Barras duplas BRL/USD com eixo dual +- **Top 10 Clientes** - Barras horizontais por volume +- **Taxa vs PTAX** - Linha comparando taxa cobrada com PTAX + +#### Filtros Disponiveis +- Data inicio / Data fim +- Granulacao: Dia, Mes, Ano +- Fluxo: BRL→USD, USD→BRL, Ambos +- Cliente especifico + +### Tabela de Transacoes + +Colunas: Data, Cliente, Valor BRL, Valor USD, IOF %, IOF R$, PTAX, Taxa Cobrada, Spread, Spread %, Status + +## Seguranca + +- **Senhas**: Hash bcrypt com 10 salt rounds +- **SQL**: Queries parametrizadas (previne SQL injection) +- **Sessoes**: 8 horas de timeout, armazenamento em memoria +- **RDS**: Acesso somente leitura (sem permissao de escrita) +- **Isolamento**: Cada agente ve apenas seus dados (`WHERE agente_id = ?`) + +## Tech Stack + +| Camada | Tecnologia | +|--------|------------| +| Runtime | Node.js 18+ | +| Framework | Express.js | +| Auth DB | better-sqlite3 (WAL mode) | +| Data DB | mysql2/promise (pool) | +| Seguranca | bcrypt, express-session | +| Frontend | HTML5, CSS3, Vanilla JS | +| Graficos | Chart.js 4.x (CDN) | +| Tipografia | Google Fonts Inter | + +## Troubleshooting + +### Erro de conexao RDS ``` +Error: connect ETIMEDOUT +``` +- Verificar se IP esta liberado no Security Group +- Confirmar credenciais no .env -## Funcionalidades do Dashboard +### Agente nao consegue logar +``` +Credenciais invalidas +``` +- Verificar se agente esta ativo: `node scripts/seed-agente.js --list` +- Recriar senha se necessario -- **Autenticação** — cada agente loga com email/senha e vê apenas suas transações -- **KPIs** — transações, volume BRL/USD, taxa média, spread, IOF, ticket médio, clientes ativos -- **Gráficos** — volume por período (BRL+USD dual axis), volume por cliente (top 10), taxa cobrada vs PTAX -- **Tabela** — todas as transações com data/hora, cliente, valores, IOF, taxas, spread, status -- **Filtros** — período (de/até), granulação (dia/mês/ano), fluxo (BRL→USD / USD→BRL), cliente -- **Dados ao vivo** — cada refresh consulta o RDS em tempo real -- **Dois fluxos** — BRL→USD (`br_transaction_to_usa`) e USD→BRL (`pagamento_br`) +### Graficos nao aparecem +- Verificar conexao com internet (Chart.js via CDN) +- Abrir DevTools e verificar erros no console -## Stack +## Roadmap -- Node.js + Express -- express-session (sessões em memória) -- better-sqlite3 (auth local) -- mysql2 (RDS read-only) -- bcrypt (hash de senhas) -- Chart.js 4.x (gráficos client-side via CDN) -- Google Fonts Inter (tipografia via CDN) +- [x] Autenticacao local +- [x] Dashboard com KPIs +- [x] Graficos interativos +- [x] Filtros de periodo +- [ ] Exportacao Excel/CSV +- [ ] Comparativo periodo anterior +- [ ] Dashboard administrativo +- [ ] Alertas de spread + +## Licenca + +Uso interno CambioReal. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..6f7a90b --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +echo "=== BI Agentes Startup ===" + +# Check for Netbird setup key +if [ -n "$NETBIRD_SETUP_KEY" ]; then + echo "Connecting to Netbird network..." + echo "Management URL: ${NETBIRD_MANAGEMENT_URL:-https://netbird.cambioreal.com}" + + # Start netbird daemon directly (not via systemd - doesn't work in containers) + MGMT_URL="${NETBIRD_MANAGEMENT_URL:-https://netbird.cambioreal.com}" + + # Run netbird daemon in background + netbird service run & + sleep 3 + + # Connect using setup key and management URL + netbird up --setup-key "$NETBIRD_SETUP_KEY" --management-url "$MGMT_URL" & + + # Wait for connection + echo "Waiting for Netbird connection..." + sleep 10 + + # Check connection status + netbird status || echo "Netbird connection pending..." +else + echo "WARNING: NETBIRD_SETUP_KEY not set. Database connection may fail." +fi + +echo "Starting BI Agentes server..." +exec node server.js diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..cc1d9d4 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,350 @@ +# API Reference - BI Agentes + +Documentacao dos endpoints HTTP do sistema BI Agentes. + +## Base URL + +``` +http://localhost:3080 +``` + +## Autenticacao + +O sistema usa autenticacao baseada em sessao. Apos login bem-sucedido, um cookie de sessao e enviado automaticamente pelo browser. + +**Cookie:** `connect.sid` +**Duracao:** 8 horas + +--- + +## Endpoints + +### GET /login + +Exibe a pagina de login. + +**Autenticacao:** Nao requerida + +**Comportamento:** +- Se usuario ja autenticado: redireciona para `/dashboard` +- Se nao autenticado: retorna `login.html` + +**Response:** + +| Status | Descricao | +|--------|-----------| +| 200 | Pagina HTML de login | +| 302 | Redirect para /dashboard (se ja logado) | + +**Query Parameters:** + +| Param | Tipo | Descricao | +|-------|------|-----------| +| error | number | Se `1`, exibe mensagem de erro | + +**Exemplo:** +```http +GET /login HTTP/1.1 +Host: localhost:3080 +``` + +```http +HTTP/1.1 200 OK +Content-Type: text/html + + +...login form... +``` + +--- + +### POST /login + +Processa tentativa de login. + +**Autenticacao:** Nao requerida + +**Content-Type:** `application/x-www-form-urlencoded` + +**Request Body:** + +| Campo | Tipo | Obrigatorio | Descricao | +|-------|------|-------------|-----------| +| email | string | Sim | Email do agente | +| senha | string | Sim | Senha em texto plano | + +**Response:** + +| Status | Descricao | Redirect | +|--------|-----------|----------| +| 302 | Login bem-sucedido | /dashboard | +| 302 | Login falhou | /login?error=1 | + +**Comportamento:** +1. Busca agente por email no SQLite +2. Compara senha com hash (bcrypt) +3. Se valido, cria sessao com dados do agente +4. Redireciona para dashboard + +**Exemplo:** +```http +POST /login HTTP/1.1 +Host: localhost:3080 +Content-Type: application/x-www-form-urlencoded + +email=agente@cambioreal.com&senha=minhasenha123 +``` + +```http +HTTP/1.1 302 Found +Location: /dashboard +Set-Cookie: connect.sid=s%3A...; Path=/; HttpOnly +``` + +**Dados da Sessao (interno):** +```javascript +req.session.agente = { + id: 1, // ID interno SQLite + email: "agente@email.com", + agente_id: 76, // ID do agente no RDS + nome: "ValorFx" +} +``` + +--- + +### GET /logout + +Encerra a sessao do usuario. + +**Autenticacao:** Nao requerida (mas so faz sentido se logado) + +**Response:** + +| Status | Descricao | Redirect | +|--------|-----------|----------| +| 302 | Sessao destruida | /login | + +**Comportamento:** +1. Destroi sessao server-side +2. Invalida cookie +3. Redireciona para login + +**Exemplo:** +```http +GET /logout HTTP/1.1 +Host: localhost:3080 +Cookie: connect.sid=s%3A... +``` + +```http +HTTP/1.1 302 Found +Location: /login +Set-Cookie: connect.sid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT +``` + +--- + +### GET /dashboard + +Exibe o dashboard de transacoes do agente. + +**Autenticacao:** Requerida (cookie de sessao) + +**Response:** + +| Status | Descricao | +|--------|-----------| +| 200 | Pagina HTML do dashboard | +| 302 | Redirect para /login (nao autenticado) | +| 500 | Erro ao carregar dados | + +**Comportamento:** +1. Middleware `requireAuth` verifica sessao +2. Busca `agente_id` da sessao +3. Executa queries no RDS (BRL→USD e USD→BRL) +4. Serializa e ordena dados +5. Gera HTML com KPIs, graficos e tabela +6. Retorna HTML completo + +**Dados Retornados no HTML:** + +O dashboard inclui dados embedded como JavaScript: + +```javascript +window.TRANSACOES = [ + { + fluxo: "BRL → USD", + cliente: "Cliente A", + data_operacao: "2024-01-15 14:30", + valor_reais: 10000.00, + valor_dolar: 2000.00, + iof_pct: 1.10, + iof_valor_rs: 110.00, + taxa_ptax: 4.95, + taxa_cobrada: 5.00, + spread_bruto: 0.05, + spread_pct: 1.00, + status: "completed" + }, + // ... mais transacoes +]; +``` + +**Exemplo:** +```http +GET /dashboard HTTP/1.1 +Host: localhost:3080 +Cookie: connect.sid=s%3A... +``` + +```http +HTTP/1.1 200 OK +Content-Type: text/html + + + +BI Agentes - Dashboard + + + + +``` + +--- + +### GET / + +Redireciona para o dashboard. + +**Autenticacao:** Nao requerida (redirect acontece primeiro) + +**Response:** + +| Status | Descricao | Redirect | +|--------|-----------|----------| +| 302 | Redirect | /dashboard | + +**Exemplo:** +```http +GET / HTTP/1.1 +Host: localhost:3080 +``` + +```http +HTTP/1.1 302 Found +Location: /dashboard +``` + +--- + +## Arquivos Estaticos + +### GET /public/* + +Serve arquivos estaticos da pasta `public/`. + +**Autenticacao:** Nao requerida + +**Arquivos Disponiveis:** + +| Path | Arquivo | +|------|---------| +| /public/login.html | Pagina de login | + +**Exemplo:** +```http +GET /public/login.html HTTP/1.1 +Host: localhost:3080 +``` + +--- + +## Codigos de Erro + +| Codigo | Significado | Causa Comum | +|--------|-------------|-------------| +| 302 | Redirect | Login/logout, acesso nao autorizado | +| 500 | Erro interno | Falha conexao RDS, erro de query | + +--- + +## Middleware + +### requireAuth + +Middleware que protege rotas que exigem autenticacao. + +**Comportamento:** +```javascript +function requireAuth(req, res, next) { + if (req.session && req.session.agente) { + return next(); // Continua para rota + } + res.redirect('/login'); // Redireciona +} +``` + +**Rotas Protegidas:** +- GET /dashboard + +--- + +## Modelo de Dados (Sessao) + +### Agente (sessao) + +```typescript +interface AgenteSession { + id: number; // ID interno (SQLite) + email: string; // Email do agente + agente_id: number; // ID no sistema RDS + nome: string; // Nome de exibicao +} +``` + +--- + +## Exemplos de Uso + +### Fluxo Completo de Login + +```bash +# 1. Tentar acessar dashboard (sera redirecionado) +curl -v http://localhost:3080/dashboard +# < HTTP/1.1 302 Found +# < Location: /login + +# 2. Fazer login +curl -v -X POST http://localhost:3080/login \ + -d "email=agente@cambioreal.com" \ + -d "senha=minhasenha" \ + -c cookies.txt +# < HTTP/1.1 302 Found +# < Location: /dashboard +# < Set-Cookie: connect.sid=... + +# 3. Acessar dashboard com cookie +curl -v http://localhost:3080/dashboard \ + -b cookies.txt +# < HTTP/1.1 200 OK +# < Content-Type: text/html + +# 4. Fazer logout +curl -v http://localhost:3080/logout \ + -b cookies.txt +# < HTTP/1.1 302 Found +# < Location: /login +``` + +--- + +## Futuras Extensoes + +Endpoints planejados para proximas versoes: + +| Endpoint | Metodo | Descricao | +|----------|--------|-----------| +| /api/export | GET | Exportar transacoes (CSV/Excel) | +| /api/kpis | GET | KPIs em formato JSON | +| /api/transacoes | GET | Transacoes paginadas (JSON) | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f049b8f --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,349 @@ +# Arquitetura - BI Agentes + +Documentacao tecnica da arquitetura do sistema BI Agentes. + +## Visao Geral + +O BI Agentes e uma aplicacao web monolitica construida com Node.js/Express que serve dashboards de BI para agentes de cambio. A arquitetura prioriza simplicidade, seguranca e isolamento de dados. + +## Diagrama de Arquitetura + +``` + INTERNET + │ + ▼ +┌───────────────────────────────────────────────────────────────────────────┐ +│ SERVIDOR EXPRESS │ +│ (PORT 3080) │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ +│ │ Static │ │ Session │ │ Auth │ │ Dashboard │ │ +│ │ Files │ │ Middleware │ │ Middleware │ │ Generator │ │ +│ │ /public │ │ (in-memory) │ │ requireAuth │ │ (HTML) │ │ +│ └─────────────┘ └──────┬──────┘ └──────┬──────┘ └─────┬──────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ REQUEST ROUTER │ │ +│ │ │ │ +│ │ GET /login ──────▶ login.html │ │ +│ │ POST /login ─────▶ authenticate() ──▶ session.create() │ │ +│ │ GET /logout ─────▶ session.destroy() │ │ +│ │ GET /dashboard ──▶ requireAuth ──▶ fetchTransacoes() ──▶ HTML │ │ +│ │ GET / ───────────▶ redirect /dashboard │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ │ +└────────────────────────────┼───────────────────────────┼──────────────────┘ + │ │ + ┌──────────────┘ └──────────────┐ + ▼ ▼ + ┌───────────────────┐ ┌───────────────────┐ + │ SQLite │ │ AWS RDS │ + │ (agentes.db) │ │ (cambio_db) │ + │ │ │ │ + │ ┌─────────────┐ │ │ ┌─────────────┐ │ + │ │ agentes │ │ │ │br_transac...│ │ + │ │ - id │ │ │ │- id │ │ + │ │ - email │ │ │ │- id_conta │ │ + │ │ - senha_hash│ │ │ │- amount_brl │ │ + │ │ - agente_id │ │ │ │- amount_usd │ │ + │ │ - nome │ │ │ │- exchange...| │ + │ │ - ativo │ │ │ │- status │ │ + │ └─────────────┘ │ │ └─────────────┘ │ + │ │ │ │ + │ LOCAL (WAL) │ │ ┌─────────────┐ │ + │ WRITE + READ │ │ │pagamento_br │ │ + └───────────────────┘ │ │- id │ │ + │ │- id_conta │ │ + │ │- valor │ │ + │ │- valor_sol │ │ + │ │- cotacao │ │ + │ └─────────────┘ │ + │ │ + │ REMOTE (MySQL) │ + │ READ-ONLY │ + └───────────────────┘ +``` + +## Componentes + +### 1. Camada de Apresentacao + +**Tecnologias:** HTML5, CSS3, Vanilla JavaScript, Chart.js + +| Arquivo | Responsabilidade | +|---------|------------------| +| `public/login.html` | Formulario de login estilizado | +| `src/dashboard.js` | Geracao dinamica do HTML do dashboard | + +**Caracteristicas:** +- Server-Side Rendering (SSR) - HTML gerado no backend +- Chart.js carregado via CDN (sem build step) +- CSS inline no dashboard para simplicidade +- Google Fonts Inter para tipografia + +### 2. Camada de Aplicacao + +**Tecnologias:** Node.js, Express.js + +| Modulo | Responsabilidade | +|--------|------------------| +| `server.js` | Entry point, rotas, middleware | +| `src/auth.js` | Autenticacao, bcrypt, sessoes | +| `src/queries.js` | Queries SQL, serializacao de dados | + +**Middleware Stack:** +``` +Request + │ + ▼ +express.urlencoded() ← Parse form data + │ + ▼ +express.json() ← Parse JSON + │ + ▼ +express-session() ← Gerenciar sessao + │ + ▼ +requireAuth() ← Verificar autenticacao (rotas protegidas) + │ + ▼ +Route Handler +``` + +### 3. Camada de Dados + +#### SQLite (Local) + +**Biblioteca:** better-sqlite3 (sincrono, WAL mode) + +```sql +CREATE TABLE agentes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + senha_hash TEXT NOT NULL, + agente_id INTEGER NOT NULL, + nome TEXT NOT NULL, + ativo INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +**Caracteristicas:** +- WAL mode para melhor concorrencia +- Arquivo em `data/agentes.db` +- Criado automaticamente no primeiro start + +#### MySQL (RDS) + +**Biblioteca:** mysql2/promise (pool de conexoes) + +**Configuracao do Pool:** +```javascript +{ + host: process.env.MYSQL_URL, + user: process.env.USER_MYSQL, + password: process.env.PW_MYSQL, + database: 'cambio_db', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +} +``` + +**Tabelas Consultadas:** + +| Tabela | Fluxo | Campos Principais | +|--------|-------|-------------------| +| `br_transaction_to_usa` | BRL→USD | amount_brl, amount_usd, exchange_rate, iof, ptax | +| `pagamento_br` | USD→BRL | valor, valor_sol, cotacao, ptax | +| `ag_contas` | Join | agente_id, conta_id | +| `conta` | Join | id_conta, nome | + +## Fluxo de Dados + +### Autenticacao + +``` +┌──────────┐ POST /login ┌──────────┐ +│ Browser │ ─────────────────▶ │ Express │ +│ │ email, senha │ │ +└──────────┘ └────┬─────┘ + │ + ┌────────────────┘ + ▼ + ┌──────────────┐ + │ SQLite │ + │ SELECT WHERE │ + │ email = ? │ + └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │ bcrypt │ + │ compare │ + └──────┬───────┘ + │ + ┌───────┴───────┐ + ▼ ▼ + [MATCH] [NO MATCH] + │ │ + ▼ ▼ + session.agente redirect + = {...} /login?error=1 + │ + ▼ + redirect + /dashboard +``` + +### Carregamento do Dashboard + +``` +┌──────────┐ GET /dashboard ┌──────────┐ +│ Browser │ ─────────────────▶ │ Express │ +│ (cookie) │ │ │ +└──────────┘ └────┬─────┘ + │ + requireAuth()────┘ + │ + ┌─────────────┴─────────────┐ + ▼ ▼ + [NO SESSION] [HAS SESSION] + │ │ + ▼ ▼ + redirect ┌─────────────────┐ + /login │ fetchTransacoes │ + │ (agente_id) │ + └────────┬────────┘ + │ + ┌─────────────────┘ + ▼ + ┌──────────────┐ + │ AWS RDS │ + │ 2 queries: │ + │ BRL→USD │ + │ USD→BRL │ + └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │ serialize() │ + │ merge + │ + │ sort │ + └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │ buildHTML() │ + │ KPIs + │ + │ Charts + │ + │ Table │ + └──────┬───────┘ + │ + ▼ + res.send(html) +``` + +## Seguranca + +### Autenticacao + +| Aspecto | Implementacao | +|---------|---------------| +| Hash de senha | bcrypt, 10 salt rounds | +| Sessao | express-session, in-memory | +| Timeout | 8 horas (cookie maxAge) | +| Middleware | requireAuth em rotas protegidas | + +### Protecao de Dados + +| Aspecto | Implementacao | +|---------|---------------| +| SQL Injection | Queries parametrizadas (?) | +| Isolamento | WHERE agente_id = ? em todas queries | +| RDS Access | Usuario read-only (sem INSERT/UPDATE/DELETE) | + +### Recomendacoes para Producao + +- [ ] Usar Redis para sessoes (em vez de in-memory) +- [ ] HTTPS obrigatorio (TLS) +- [ ] Helmet.js para headers de seguranca +- [ ] Rate limiting no login +- [ ] Logs de auditoria + +## Performance + +### Estrategias Atuais + +- **Connection Pool**: 10 conexoes MySQL reutilizaveis +- **WAL Mode**: SQLite com Write-Ahead Logging +- **CDN**: Chart.js e fonts via CDN (cache do browser) +- **SSR**: HTML pre-renderizado (sem SPA overhead) + +### Gargalos Potenciais + +| Componente | Risco | Mitigacao | +|------------|-------|-----------| +| Sessoes in-memory | Perda em restart | Migrar para Redis | +| Queries RDS | Tabelas grandes | Adicionar indices, paginacao | +| HTML generation | Muitas transacoes | Paginacao server-side | + +## Estrutura de Modulos + +``` +src/ +├── auth.js # Autenticacao +│ ├── authenticate(email, senha) +│ └── requireAuth(req, res, next) +│ +├── db-local.js # SQLite setup +│ └── initDB() +│ +├── db-rds.js # MySQL pool +│ └── pool (export) +│ +├── queries.js # Data access +│ ├── fetchTransacoes(agenteId) +│ └── serialize(rowsBrlUsd, rowsUsdBrl) +│ +└── dashboard.js # View generation + └── buildHTML(data, agente) +``` + +## Dependencias + +| Pacote | Versao | Proposito | +|--------|--------|-----------| +| express | ^4.x | Framework web | +| express-session | ^1.x | Gerenciamento de sessao | +| better-sqlite3 | ^9.x | SQLite driver (sync) | +| mysql2 | ^3.x | MySQL driver (async) | +| bcrypt | ^5.x | Hash de senhas | +| dotenv | ^16.x | Variaveis de ambiente | + +## Extensibilidade + +### Adicionar Nova Feature + +1. **Novo endpoint**: Adicionar rota em `server.js` +2. **Nova query**: Adicionar funcao em `src/queries.js` +3. **Nova UI**: Modificar `src/dashboard.js` ou criar novo modulo + +### Adicionar Novo Fluxo de Transacao + +1. Criar query em `src/queries.js` +2. Atualizar `serialize()` para incluir novo fluxo +3. Atualizar `buildHTML()` para exibir dados + +## Decisoes de Arquitetura + +| Decisao | Justificativa | +|---------|---------------| +| Monolito | Simplicidade para equipe pequena | +| SSR | Sem necessidade de SPA, SEO nao relevante | +| SQLite para auth | Independencia do RDS, portabilidade | +| Vanilla JS | Sem build step, menor complexidade | +| better-sqlite3 | Sync API mais simples para auth | +| mysql2 | Pool de conexoes async para RDS | diff --git a/docs/CambioReal_original.png b/docs/CambioReal_original.png new file mode 100644 index 0000000000000000000000000000000000000000..134ec93e3479292499924454dd693f9f669f4ad0 GIT binary patch literal 22387 zcmZ6z2UL?y(>@+V1x1lZDJo5rjv&1^L6H!OQYAD22?C*p-cb-i0qKN}29Odk(mN_e zF%&~dXhA`G2}lzV{B3-{=l#C_pK~~zrn!(?qMB!BGYKhNM^eB#G6|M9;V@t*#SmPhCD0m>`(tG6~8 zju*KE&izrMyTe^H%Exq3*U!hqg8rsInPh6RzgmDv7X7oUc({Kke1`QP_7o7~?@xZh zi;gtu+Tj>Xf!(z8;la$wr;X{=FROJE;Dl-6Gp%ystp7GuFWwTv$o8{;r<6LD@r0`C)K&%;#(W&et4=QE-`iR}w4WA8j}w#rHWZ+~^8SQyfkx0*kR zG5e!amQv?&B3_B<->2arBK)}98ZIJ!N&IHE;=7E;|9$enCRUp&7|SW_ZvJhaf4^RM zVJcADRa?s2+i?lr)M%>fA=*OwuP_k*N>hID^s-K4iuW9cPWprhf1TL>eV#DF2cBnY zXU5h((Q8PRZan=Y%J0g*gG0t2>Dh^xIQ8=8>}wI&^C~_(SmOMzMC}$eSE*BNurquv zjBR#fE~`r;NBp;MbC=BDg_t&nv$@^;W-ZYm^M9uEjn6FxI~3?4wn*ZGDDTV)pW!Fp z{BK|7Za&c0q%dRX)%Js?32nu;(@!Y>XKZEJPb%L?rq`fhF&IoH7{YP|7VWip$W2( z)yHP{XYbv|7M(rV|Fb~hp(I$RX(_p3qI*|EBA&0e<;lOYK@M>!I~Qb`X6Nzl_iNwt z#f$AN5(oafspc!M?Oga5ADx?`bJ!OLoBO{jkbmI&_h=+~*B0NuTQN#9)udeccUbkh z7A0J2-|&8OH+V+d*`xCBlklz;L!8XG8Ph`KAP!sfzq=g1_KxYp$R8t}2f-6T(hHAm zDyg&ouO$9CXgw4CzWKo$<44>jCv1iOsQ>U2D*4HH42;~~k1y{N$6t>W{~}J^!tmTN zMI0ZTY1#oyN3Y_;H^P6;l$t`XxK@tsnd+BlP94R7E#j0Wz4sP%0BhG&u-(pA7wZe| z4{!p>5N5XPlmF?}epGmCBp}c@i7|Ii5!z^^8#t0Q$8&rx7IgHldvDR^0oD&XIHz$l znnSRb_piv_&(LRkk^D|Vi)9!omE@r@58lPAA%_Hfk&pz{`XX0*V?;Y|b-KuvwU2d@ zOjG}}u;G*FQCmgh-`t(N!$CwkM&|xi>&(%;+z-HFsV6iGQIF8wjV=~a*N_qz1qkkj zU%5rlkl?AgL6>wSE!7wTzI-6!jwMZNU7&dC4FA>4lrMO#q$5+L04eeuZyjjyHqPvV zg^+~jNU-3-n-V|jNRaT=Ld*#9jAn?oJ=J1V2EsZ=su!;eI2pTAKdakH^4E-r^{$A| z_=AsvL(gNdIIKQz4kGr%q|EkzPR_dQqAoTYDk_xQa2uupnl&%g#A2aof&)JPGYXC2 z@X&wr{r>k1af|ev3-t(v;b4_%*Z};(J{x z8KVS+b;iu>oAEydduW#G2Vs8-=0WO-$j$3U9vyaXyAvknOcUm0c_Qb}KZHPjuN!xc z1ndazPzFsd>b5+2^nqkrNI9d(KEAXre5RT8fdTK~thDc-Dl{{YjwVHr@7+j3t^1n1 zPD0+abk(-lXZX3#6%K3Pz%t;*s%bnHI3eNJ;|FJuhipRMeO|{koZY+(X}34vtJP|F zqE4wdb)`{v7P&8EozQ2!KlZrX4|=5SucG5S%o91rCkb4N$C?y~>|Gqq)_9=x=FKGh zioU15lvy@$0DBA)==OFmdg$poNq~7vo2~R14RaTsu^0Ie!2tcEy?N&U{ria8d;gSZRrLlgcq`;ix zY!Gke7*T%(a6A?`kB7f2OnZ_ZdrDL|ZfB;8diuY;+F?}3o&AniqKD+*^gX`1aY7WI zcCad&0Id;BN9(az%muHnxW&Qp&ikXalU_mNC7p*rMXhOCnFCQ`ZLM!RixqZ|}+gFxl<$9N(WFIWI4w+J?` zbVTY3srDjYrf(QIJ0g$-hrp-r1KXJ|_ymW2O1_y-d_y|6uIa2IpS(4yl$KixHXH(3 zWzzVSd)^>#=Qq}!?@;UDCPY*d-1?^_nOLOOJI~TWpb{m88TT7J7z_K;s5UCx7kcW= zpGUhRk$y>`=XkOsnU<_(j)k9mIdLpp<0a{FojnBt`6X3#p%ZhOc-#gCJ^>jcnbLS7 zbtK+z-*#6^-?YZ%d=Jy4|H$;A98FL2z+KyoMx7?~pBPd3{}_VSpl+J>J37w8)5=)c z`~Gxbh%u4Kw<*$z(h^k#(}5b>9qD4cqMGy*77)R&I1GB){0TUD`<+S<^^I)o7*MZ0 zmfWF6qjC?pTI=#uOaW#2M9!9bh#PTgE{uK2cJT9Ju3gCU1EynHXTX(|h2bR{;%6b= zgJ`tXQzm|@r*^c;rqa@M8WOIB9@Qq8QRK-B`kr4AJWd#FmEs-d$8s z{;K)Pg}C`y8?>SN0|eqcTN4DJe@^jaM>8RS z04jWu>nfe9r+Los9SSc#pKhawE*05PRUrIvPLb(8xR)*pQr>R?S44;9HmXLT#7AIp zm+6Boz3o`d$a$W~+w`{QHRMyanlR&LN9him_o#BKCyEVbvjR1xPW5s+KcuGCJ%JeM zqv3K+lT7_^&wQ?Qmpc@d_u zjAPQ^)OfQ+IZjOeEJkhIe(pkPEhL|lx=NQF5Er0qeYO*Vxdk$!8^6m#g08+FKA8XO zzF6M`jze|F4XTPZe!&Yv^Efr@)1}YySSof6z+r+-DBZ5&Rc*D$FQD7bu+o~#c9wpbYF~Z--Cl*>h2RFxna%PZJ|@d zY#~=9hcJJ$u#eQJ&lM*O$&DNG?FUmfX+iawx7$J(L&xjE?Q!xiogHK-@XDmL9}BX&)!fu zsSmA=!jzzMMEE21RqRHqu1P5s0R8@T2^@RJQ*F# z=KnT6%M5!AbP0`)OKGd=Xu&wY&im`UjW2wi%V2pf+>R z$zr}k_G~tq1x9s?1xx=O%Ru+^SW7xwJouCHia_5G)nN{Q7sWJ}bhI8n!zm0*N`B=V zU}~raZ|ABX!-r+257L_%)X~CEMly-wH#0 zV9)~a%7Ht}9U}xO`Yre>nDwqyRU+o~3r+pO?;n6*Ii%oY5K`xmQn)DfR#WFbObWHq zR@^*uyruJ)%-ve2E}Wa;l+ObSe&sGM?$f@R>u>W#;*42qZ_ZJR`5XNNup;kr0G|F}epQpC0Jjcg$-EC(=#B~#T z57-uG``H%fcR%bm3U=<&?h1RbXxz^nl}oMC5BRm8{y;PbS{tAyH`H(9uG!KWu+!i| zN(fSEY);|fbmch=4zp3&e@9du9C6#GoHczO*Jcr`5Kvrx!J-C}5D)I#wTchn=Pu{p zWgLdTeqmqmkde-GtDW* zN1enFI~usi7EP9wd4Vdyx-%0ZEv(Eo=e8MVyrF$4#%b}Ni?#NM`PfPG6S{1XLdN>;yE>2Frb5z2)B*!DW4pd+;&IawAjOabC_#C(C1ms8t!1-rJwCebp1Y z)%s2K#Mvy^{t{=01&Gh4)C3H#x`a%Yru=FBh3oD2fvQ<`JNrB|x<8IYZjH|)UKA;Z z#=RXa5pUBbm|P6z9$(TwV&mL`{nc-1@~UO>iW0Q?N=Fuw7kK1JYp(IkYzZR_+I2|? zJHoJHiJgy%r~BZoRc!)YCvR_#T@%Sv^phHV>l3=#zf211i101>HHsQ7ny>sq*bYkJ za;^8}Uih9F?1Wgxc&=V%Q52c9Nf_#LdChFE2us)w=_!QoC_HfkVT(Z}t`?~i)Eg66 z-<7J`)X!3B#rL~hw$lE#+i91qI5&pnJM3z0wcGwx$;@(X!**_D;U{rZMC+URb-xB6 zJ*ic2m!5~0Zb($2q?)9GqZr2Q=V$$fli_ufeA<>dK@A~0wV=jGhcHEt;_!?VrJqLDjweQxVe@y@hB!BMnCPs$s`kYQ$XRe|Yr9*Sd)a zt!Tp|6O+c<`v}WnnQOL3h6j~#S@A5D^M7D@2?CXe=i1YpDUfw{_FW%_&i=K2;cF$^ zDcePF+j@~9{c>ApOD6Fv9e94H(0wbyRBUBLZGW7vPf>Kk-c=Pk{JCu4Rc4imziF|D zylwG*onAJzC%T%yPMW*gfIa~Y6V}%rtH0QI!!eejROVDgFu}#uaN+UHSj2Svki#0W zPw~@NZe){fj5>SW*y2vs*mNhg7VQpwQo5KTBzgv{LFAgO z7&F_`27~visnwSnHyI|_+3%&r?)nt+SLLEG{whZTe9I#9{%~9SwHER>V*0HS*a-XG zQPTGp9jwj4n~ae5rZ~y?S-3;^4oO@5`qtfzd(&6JQx0_AMrI%>qj? z()O_B1|}U@P{lCLPQ-={x3x?Z$)&)LPitcfAsaOFbVZpVU_kEvPdx!g@2 zJxl2IHchx-2@9Kc<2!jT-o^f{#*2#GkJEH24;$=0{Mo9hq=frCcInzauXI0l}E|-w?@K8(ArK`Nq zvNQUQb3UADix)_g`Bmb4n}ovJ=p&Hp=H zPK6Cjs&dIK>MJU~M$>EVD~i~CYF)qICVEos=(%@DB?}vD-^1#Uwh1@p{%U>({&0aK zwr*RCW6dO-H7MnoK*4r3Lnr3;*E{^kYAc)AIEd6x#49#6J4+R_m#QaO8xwJCJP@75$U99VKEpo(_yp-w^z7P;3VdHrZdk>C1qC3B#q+p^8 zuWlS((&8fie?4#0iv*5$Al+j`;q5Zmwj8=Ct`gyUUPG`khwbh=?=a;HG4WRA7T8dy@$KuA z9}R}{q&&vg%Fr>_7(HguU1BQ#jJIdZvYyY~DpZfPZ?d4%P{kiAsgtiJU$?bve~haMB`ZNoAf z1ewNr0A!xFFlkJ!kmhu*X-u|YSz7z}i}B zu{aNvwz)nx%O(Fm2{>LhJ-jj4oh~%RGlD}2hD@6msGu&hjVqWcGn`fWHQVsVgXQEl$yDxYR>C3B7Ra{{e0h)b zP@V2fz7=BH`ZaXWkXW4g1*@j^<^18uT^j$(Y({J`fLYGHI-GYM12dA^D_mq{zuh z5C>2pAd0EO<>EeRMmTSzxKtHvuQuM|#_Itz|Nak7rI*VIGspzo>QWiHWqB&HucVol^nsuh-mUFi^I-z7 z&moJ$^M!m;<|jLrzN_%T{CS4{j9U_ghEwcC|?*?SM(@!+0s*cx-?l+XUAMt@yMDWP(C*)xl6 zOTQO+`R~~yqDLDr{qa2I$>vodD{K11H&N3(JJCsfEF${jZ_T4)wX`=Bts_JFg?xej z$;2~XU~3pZZynoVDx{AtYs-sbvi|)Tht*1IMZ}!6^1k8Z&vJ6&<*_KqDvEQc?U8C^ zJDOJ|HR)y;WpA7;v}%`@(sH7sXp531D0VS`ufa`&K6Q{}AX!y{Ng#W!jK+~o_xxUw zPM4mI>6Z;y-mD8<8=KHyhS9%YMn*ZJN53Su?bHuy%ILH#Py0HT1;6s=bo=<~(7vr! z=AX<(nZd^XH{E#T8-LfI^@u4EF+=M)?JdkfTFLLSdiX)3-~wD2dYUK0@65RKqtJnr zkzNOj3CWtucVT^5qZ+F%23`GY8uTF3f|;IM$g_62tv$@@E5lcEB1J@R8m0Sf`HVd^ ztYnaC8g`Q-FW)l7*3&mmsJuJWPR_VDrl0^#m|(CkcADRm+^o0qt<1AQQ8LSmh_`Dt z7QP(cGCkKnWtUDwz<gcx2GpQ=O}gdXU3Q*wK8essHUq1yvWlX)Kc2&{{&)EfC1cxi5kI5 zY8R3RtIe+zo6l{$WnZJ6`mRT{{%?lk*+WwtmcT8XGYd~ClZtz?lez55us5~!R@}O< z^RvV2#8q>8TQqBIh19u_+<@ka`vMO`VW)<)*vLjn2#mk%-8G{2g$lkyIX=;m&+}Sr zPJf9%xvo^ZHRmu+t+;E4P|B3p>m@=8XN z`g+IrYjXeP0+?LHH$Q>Al@hLfdAo5VS8X$ovUL~jlWT9Q9G%kqNn!MjWc=_6zu4!K z%g*hJ^WREJAM&B9_&Wu`hT4^x9x z6A+B{AKrHuvDXZ*StLdheuUL3ISL>`pWEpeo%$^N+P8Z_uW8u*#aa z>Ry)?TE#T=N>7Ln%JVfCDbtn?S2k5=$p4I=N$iarAt+@KF*?sCo?i<{mZ$uubs$74kcQOS)dqTQi!hR2xI_nxA= zzGp^oj}0{9p%tg@OtFp1xz^qmu@kV5tvVa6KJ_L)a^%`;{LERY1R}p&tO@*fa~l^* z-%XFR>sC^qdc!B_32cwWC!01kuWr3z(_O35=Hq&+3iarmvI*8mVr)z~fM1iw?zd$K zp5eK!Nx!+ZtqE-_X_i^K2Mz%W?tK1j0)WGGetuE<)0Q}05lK+>7$|o)v%UMHgQYzg zh*u&NM_WB=Ng$p9{z$AFZ)!$L;OaDbt;)?oCe; zzMFdin4Kt)y*d|Si%8~j-&vRKl^=OCl!v0XO&i08ev)4?&aNAVF2a|6!m6H+1^hl= z<_Uo6l^Ip#$z)TZ_i{#;mWR3#vUuflXrZYVip9j@cVuHB*;jwEAT;+2moP;kqk+1l zKdk=xobe##dBh%EO>7w>a?!QBQL~64|7iSjL)1AR%{51UwoYE-(;Ezc-PVagyUdpH z-v<)E8#+O{gXocKpx8?`T!3P0m_8_@B)wfRl5E$Xr>0f^z|>#PMr~<<-NL=-A!K)E=x|%JjVDT8AU9*6ntEnlNf?;Pi=A4}AuWd=w+< zjN#9|um}B198WMO-l#U3D1XFuL?w44E_F8=AumX+Y8%3wUS+s&4R$~D60jD?(x5k* zRbueON_~F+;YT2uLRc&ot02(|Odt*x%+5ij(;fMnFF@akfy^TfMeiG)G14FWXusq?Fe$mjP| z^!fdI1vmZgH2eOPtSb0fqrnb*H_t_>X;>GJ!C?RCDA;E-+`*WnTW53N?Yb-!Vc-Zxg^l5!E(Q z(~w6^){}xDt3sBL#`H<+3pGX5CGk%;j3pbRyKZiZrTT5;Hkew!)}7Inic8jOESf`Z ze+hd10O4C9f9|?W3Ey5V4@XJ%K~!2i}x1C?>vt^79-~oVctPq2q_s<a1atW?f zNsr=X6qc8!Z7YICZHqi#(wSIHrYr~KI$Rb|iteBY&fF*6)Opm)d-ytdo+Pu_a{ z)mQ%K#RJafrl0_R?kT&Bp!SqU&BXrC0 zNg%-B*FW;Vo_tCAFhZCP>5EDfp1I$W!kXZ9y>`m4US;j_Dm~BQ9Z45E1Vd-!NR*#$ zkTjU!5BVM0iWZSj!-ED{vju;XSz{F#!c};2YJ4~@DrozQYTM{vw6oN3lumuxfyi5P z>M@@@|KNjW7L0l{@Q#zSWrpdpb-czR?ghXgkPJD=e4RL8!U9{10b+` zwiFHRQ?9U8X+r=;TYKoL436(m+bxb+O|WVI=7<>)(C2Ph=r__F`=RP&`Ol;8#rd zWKpI|Hw`=fiNPNBG`tKJD@~u8Hdb?w+*@GLrqku@7Aw^GiZz+J!0PO22b;KtNL`z1 z{^(zJ0r$^ulOO{m6d=OaWrgds-F!6Nli)%zKCk2>I7&xz2JS3w3|2bmCt`72)b{xR}PvM*4+}@^NcqWSPgilrgjI7hme4 z!DPHeE5zIA033?5{b{X@pGabJ`=EAU>rFsGi}_*N?X6hymqPUMhw|ZA8gbb(&{A71 z$!dyu%80iSBnUyQ$9P}RK?PD zIU?1P)Y%-n-Ke5eX&WKERHy8b>stsz5d%9|tlGy`m9|fmv1KF{lj4kZ=gq9sxGnqk z$<0Ap%!yqc9QJY#p#(ERGX03d^4nX8@*QGHW{p3f(}~j$zf*A7G+>IiBUVUATSTZmsU|TvPEO;4nzWJvq8u^G(H4r_&UmaRdJ#$NZmMj}e6e z9F-C)$1&v<7vpyTh_HYPw_=AZ15+z1QCv1i9enQ39nTk%T3s=HS+N^rmIquAvVbQN z_X&NG6Twl&CTaS980<A0m1yu` zIXR!y`h$bb-S@^Ku}mB!lYd&5Onxi0bIDCuLimhWa0;4w9!k{?zc|hYkF^qngsWZ* zqzgZUnI!w6pO#6+4GJ`@X&y8VSj=A*6#Y$m;4AWnHPTRl_@~u++4t+b4D5kpoBx?W zmXpaiENhaONms=a)%sNLP?nQ0iaPlkpKDQ)SnxF`ECZAc>=G<9Z6Bp&+5i_Fkh}nL zn@AznBArx`OYiW~`el(!tO(yqWfxTeyL+4mJ~tMdPl(bNdY!kRLVD4T!WGeW*UTyP zytTZG0DBN>kTJ?_9q}jol*~I7qA>i6H#gg#6(WxhrUFMe`=#@aA-m#W1lR&_5i;w2jYhd#F^(wqjCzvp?ZQaePp4 zZ(STOH7uWaMc{7>g1#;j_gH27Z4Fqzw}z*Juoo=zc&&=0)&a>n0AV(26h;RT{6bKI zG+V?+!YWKtT}?##H!U45(jIz})2d#9MCGjU2@rv)D-{wqpn)7q-!prQI9ED=#p*=W z@x}oPsEDb1BIeVKmPAR-7UM&}FV19bx2NGt%4lyj1$lfCEI^@=A&YV(_@Z!B5;W=x=)zuP#cO~|vi2uYR}k{FG7mHO`B zj7nN5_UR%{{~@mF4CKnJ3NgA0x+_4m^PIYn_(gX{ytut$#L;<}9HDHeYqyMMWN~eG zbbMlXwQS7aLq$s#3q?ZJO6&T%YZcjsE%XE%bjl2a^*kbweQ1m`vh3=GyhpbxKITT4 z`rG-9-VI)ZK&%0$rmq2W`!bjjQFIEGJMMz`$eI%q`L4Ww$l3mCBL;)FA=6wrC#94q zZI~@JSa{qGo=Sp`zaeMFE@g+*8=@l&bix)}C)#n@E zsA;ql<`(w39#iBE>ftk3A}r4&y4M`+U7x z-{pdZ3)OEzCYTDDdL(`e%yQ|hss_aBX1Y~Di|^_L8?ZaTl$7|1dnAhRr~;t_KjBAW zuKT9ZK*DQ!l)||_6HaL0h1PvtRScGe$|J5ed0UU&@*GqefYVYSDd z{G!+jw%>1ZkvSkOjTi7HQ@4r%?$_Ck!_L`T;54hlyjI0I5tr#yY!UMaS*a>YY@Pu3 zEknR+ZrAjjv>$M?;XTU$b*3>#n>!$CrURUKYX$;4d22_2ujR{fQXnZb*?Wf505(wu z@*lv^%_XS@c0UeNNH#9sdD-5Y<4+g<6Ac_Iv0h|J2ELJrofiTL7y>>*&VA+dlpDCt z>|-|?61ObqZ3)wx)uBma?KE*gDFd%`m^vSYhtQ+&*k4&R=sZOIcUOi2E zKmsM(0y^YnKMDvRIfc1sHGNoPjV(Jz?7?o-pMH{v`IEdAz5lgn9Zm=XOl5#6rgLgL z=Lm8M!VJp}8JR`u`dVmjvNlwRHt*tOq}U6W$}6u@DH^pO4+RE7!T_=r#a}aU&Mm8} z7x}856=ulLDXfaC0qZnnlr(>fP#GJ7qGW#@D|U~ae|~7=EGNIq_=p>D&RJJq>3CgP z><4!cIU?U&!J;_h5>|7aKjAz)JB!Kz`6hI24h<;LD^wR%^?A*G<_eN&t5GOWUGWD+bu7&q|XhZa5tGbzkco*DN6YP*Ul_LWtYa?$vSVf8|yg&B5nP! zg`}aSGf5Vcb$4@b9aCCd#|-TT2`@fDQE5vgL;CMcRKF?;t~{$g%U=g9TD}rtbST}9 zla-UO2SzpCmNqyCB>-yl^y8(N>1eI^6B9n?m%Cpt$CT^w)+k2*Fs&0!oo~DkENhhQ zol9WUaL&}siOb;H>T+1@OHG2?pkAE#F;vzuVpQ=V-&FVA@0Y?&`n=e%;jX+o+`z}% zrR0m1b=uB8te>%KaP7uV;(a(Y=Tjy{qAv?2nX&3{g_Fva3!moH&Mf@FiqfyXJriS^ z1YdYZ7O2fl5^h@B>fU3pi4}d}D#HxLiXAA{4!@&(9%t-nc#StGwcAKRw2HSU3mm^`?PuNAnMhA!A##SI57Dq|adR<|L? z^!9zMS`Mw)W2Tuj4u}3cL=wz+4S{CCSd^xk1ZBnu>e5P%xCn-?J~ zYk)q zU5shi);(Ow@F|Xt^f+8-!zz{hPL<%22FdQrz&+K@PA1&x22K~S8zAGg^UAb!ccTIG zMc2MS#kt1aE4cU~(bBLN!v>gg!K|wW74gTTFLgnznKd%j4uUdA?-H~Lw;aUsxuKcD zUFZCrrcoiKJH6Ol*D90_mZUJNm!&PqRTFVW8(APhy<0;oLVP7^O+x;Xzu#6V&fHJc zMsCNVdxvt_DN3EUMqW$~L&8NLLao%HO==ag{wlXWQzb3<36X?+~t9T6haac zXAPy_N2-iBgfzd4jA8HeDaqUwkv-c?t1cJk>hq>v{#j^~?kRod>w0i6J?4JHMdiSD zJiqfD6N()4&sm-`fD6XTNCUBO{&MncwE>0VB1Im*0WjmUJdqThNEEa!sQafrvNSUN z;AHE3c?4x~Hf!2v7Qovk^tL*N3<+TOjl@`BO<&#VmPY7Y@`f*UUv}VN&?iLH=H+~> zPut34`Vc@L(R&dRF59%h2%&u7SId23P0xcjMtCig@LJ@X%1?TICu=Fr*peQb=$7=Y z+viYQm+l<<##eo{gA687;k(j)M~MRzp9HX++^P_>zy?#66G;k0I;n148#EK+d<&RU zQP-hQ5m7mZrF=Pnc9wydHGOz1=oU9}rabe+Z8GJ9bUi9noC zg*GC`513Z9>==Sl;Y@waGwRF1fRqn>$Qr*Z{&fvMbI^VuP)uT+O!Z9;sCI2kmVOmQ z19_0(Rby^HABqOpUEo7Nox=eB8sBf2B5GM%LP*>v{`EsZvooSx2}EK@5+%3lVo}~} zsJAlkf9$3Z!n<~{h+f0#3nz4%5N=Y8=KF5fls|g1&9gxTcxxb#AdppaL_1}-KDgtr z+E98q9o_YQS*yoSYqM1s9yo#$a(y)ZYhoK(vMd0Y$QP2}{($vJ${Kq77GtIG$)$zi z^lYXBhszKl=7g_YuQIs>^9N93s{y-J`ay;h2W!ZV2yqkIq{-SGBru6eXr3VvBJNj` zP7oCZ5T+&gs+tN-`fHRccV)q-&y8a~qUHU2!G$cZTyMGg!k$xGv0t7`QhOM#(Ep%0 z)i{dDtJRkVawv(Hpl?F&ij5=Ez5fh4Rxhs<4Do#*Q;HX@!8w;@5M_vpasam`M_^cY z9(SsjO-`CfU}pBV@amz*JCgd{yg&@kR=sj&K$u-4qc6AUy)P_m|e@qgcp5!`4QZOuJoXNwrgUdec$ zhltlNW(saP?b=s3V|@(EFfmsk`Qjyq28>9;BdrRlaFFJ$W?`{VNnPuNjg!V|O9A;f zTofRiTXEs@LhTQU(?CBC?vBNs{=JbYWVV*(Hi(}oPyqPQ1nhDmU^hN|>Efrz zu+vNRfGH+tx-WIr&yy=_#BL9di2WtG{AA3J2C7o)-7P^w0_A(8&)|~5NKxePX1~T^ z$Goea0SYI5n*&EuDR%85*nS2!LJb{ih+zE z^JU8cfnb802+O)NTl#Y&ohCpDFg?=xP=-GtCYHmd&*9VQTl+J;`Z=gun30Y0;=L-CTNMZ*r;I z`3Vr$r^xedQ^QZ+LsE2Yc8y68-P=NH8C7EfkkXziWwEH-bC6(HNTbgDt5=1z!uiC) z8W|c0v#{&i&%;kW(rmL^%cSCEKt_n&gOM|IdT86;tou^gIU7UjXwTD>-64wLLL@)o zJ-)LlZF20DvQe}|KmNt?T?Mb|$-P`#z_rKJ-w%6fn@wO1d!E8NhH`zWum%gN*rMRS z#_s2q9!>)$h<`K?On6f&!cFAYsY<#PK!`{3)x7v3k*N9S!G?0nh%yNGvS@4|b6$$fa_9OrSk zJ=oXASvCqJQ@qwhba6;2{^@Vv|JHQ3+gpJ8yv>V2{h^zT`8?@8gKGsscfeI`6GfAp z`~j3dAbf=u?dJ;4F(+G?jJ<#A+k+E%o?`q05{9+;{^~Rm&U?Mwtzjm zMofok38F{MZ(=wKt(QHYg94=X?yyP*DIT0vB-Cu3;;m*k*F`t^~fPv}RAGF8Ge{-c=P9>GBmD}BbDn-wlg zsx{v&*U^3urF!wIsNDbuJ;|ybytBijRBny?(kfcC@s0a?blkvb@N+tAKJ!(&I3D}M zw7?LS%3l2ahPGSHh~&qMAJ5oRi*-tsU6BzABjnb0WG50&bq6~0?SDNy& zzk~q>y02u?RGjQ*tS=mK@tPllE=1iRV>AYH4*m2s{0p0jg%Y^A8d%F50Z5@qs9DSM zqf1iF8htmk{Gb%yC|wl=$fo_OLCONXxcH?SoIe}eBKb7wQjBOb zpngFDpYS@rH{@O{v8P|&IhoxsBvq&n>N8hU*8~uMUuLs5-{zu5u^fgQ?&dbN3QOEX zO*{_h9t!v!qL8#@rsEMerW|b$Q(i)?B5Ja87@=zk*0)UMsdjiOk=ZW85p>gz!Tt6- zng$MVuYvkK3Y$iK3Z<{W9xBrm9vGb`2YE0hw*B+fuyyw<_!qOW{M1dZoqCbgdc!YR z%Rgz$TU8o+I=V<`A9jea{AWz+sftq4mI;A?2bC&osXrvEKBjbJV(zmdSeY-FbF(Bo z3UE25*6k_w<&3@b_aQt$2lIo$!cybg6!&?SEBAOvC$8=799aw0Y4;oeW`Pknw~zta zMUK?xx*zXhhq8ABK$ElT$D6H zt-4;A(fYZhY+`GbX`UmDJ1WlT*w3~bbwa_OZa-`MPDx%)HGn|a&Qkv`z!WB-p}%}) zA$DyKR0uH_fO>=$qmnE+=azpo?iXdHsBwOjf4NnR4mri+Z6}9JCj3BhX^{M8jVdPR z9TBn*>284HMkkt8mo6RwW0x9D;2ioU&MUTUs^gpsNj;m@9$Bv?ZzciWjZYEoWw+&? zwc3Gn;!J=As4~R6@#X|}zI>09Jr6_hcg9_LzGVh$vBY`5$*aV2W$lQ_+(&0v)9HmA zaI_XF{I33dlMphgyH~oN2;!LL+c>#VK~t()vH{q9lx#z7Ppyba{;wVn=s zs3oG&e4DDb^N)~aBV2&q3A?amL6K+noBOTDJ)c*|dWNCXltFLJ`pfp^jOx9C??ewV z4#=-DZRgpEue}-}2l&*s!ky>|QM7OBTEkq=ZfFA{8NQ3@xEnKnoAX%}pn1JMgK9SH zFCF(5c1nb6u1V(^VEOMj<;=K)xKsMPh5tPru^-MKAxOK3Iwj$~+`5{@*J#m$&>hpp zuVpL;(er}c{WbKS`5Gd%1hJYig)@)^(PGSq0PE7YfM|6PFGm||?dMs;y)nhfvVkDw zW@rh+rYk8!{9e%9m_K}YefP%p;U*~plaQ2#kPTYS?CnB34W{7t^XymzUFm|tJ6Nra z2yrrvg+PH05?SelYv!LGXm6}w75Od%zQ;dZd@xZs9cSI=d%~v+r<$^b(vzD`kA-$k zs7%p7V%b%h{kG?t8K-1JijI?S)I>L1Qj6rC^9 z#N5EHa5I_45?L$u@c}+Z8%Aa|n{BlFSjNxvGMF=G3Uc|cgfMbK zwhYHC#f^?Ot=5LuHQ07O%#gR$MGf2(cx%?QkL_xdmmtV%`?>7gVt?_gkM}8wqz^9CAest{JZK~I5vN*a{67G)xh^!{uK!U|Xm*xQ zLn40v`g{(IvFQ`MNwE6sW>Cpozbq|8`I@V<`KS4ns8hoNtLdfMZX8cl?<%wV{t(}( zBBY?dRJ2(SHcqZBBv30ZB_8~N=T5+tjC56h-{5;uL{{1`w0oG-k?GFy zqH#?iC#b;ILMmF~9CHw=zZaPiv>vxq4#x$qy?!%rY#@Zq6}g#J@!6>J>`5*YxS_I{ z%MWQ;GLOlK9;fE7mwjxW=yWwj25G;mzhoo7k>}2ur^0J|#-8>!4<)UGTgl%LxWP_O zCBjXqHm{nC!DC(8*fLnXJCX^3;Gb0+oK_h*%u*}LaA)=_7bCW321s8i-lOG+c_3uZPQ6|&s!s}r`|y~b;!RYo;4 zm(>iFr|MlNmG;*@;q?t2tgZnzO81`ZHxT0G%G9z9!`G=buDwFn8)}?#7WR1#mDX4; zSh-AbADRaSzC*n_LSFrUMO=9xl->8A>V46s&6YK?jfg1OhSYdNS+XR?7Mdbs$-c}` z!epr|*~h4uh#_R`YmF^T$v(D*NVde-m+zfPzu)=uK4As1WUCXLt81Oa(eu}tpEvGPF2262l{{8ocEd{ z4ie>yy)Hj^nG|LDb4~ZlZGX@~SiBw||4*=$z?&x4sV2g6zOAZbnZv8= z)`A+XmowJMEK<|D6aG|CIve5hWp-I zE)!IivYFVF>-?=>aUSI8^k^MZ@UpmkKmCpqbT>lGpyee;D_hs?GJS_l5P2tJQ3$qw zgVk@@1oZN0s=I7A#cxWT)FFb7w=U#z%EYN;Og>g@4)S4ZHv_$QM3p9zwN2I6^j5{BRK4N*UxwH zf#u6jU)#HIo;d```4+kph9ivooB_Tci&=TPPs2o+MQ7G>E$uSUZZG8hf1py6?VDw% z&~Rafa?VymvnmL&^SYgxdYpN)G-hxBVBo;rzRs2#V!K)k#`WC56;BZ*S~`cjqM6s% zdgqm|!Cp@;&_sA?n$di?^|najysu`;JIwE=pm+dgA^RC5&omdcU1rH%Y#@M<{$NU= zj$PmQOj(b13sG0Jv`QeUU#{sAU#iYp^6O$oud4-g)>A!ZiLApUKQk>{DSy(MgTpCGBwNZQ;08s&j(~yi3qx%g{GDqO_ zn`XGkmTp3Tdj~%x-l5v?!UFq0>P8R3qHj^xa4UzbV-@$efeUOsI1N;X_)3eJO2s^< z%>GxX=f2$Qsq5n2F+g!4^TH*-&68nB$fXF>G^pq$_jrlT4}Li9*lF>E?;2E{^@V81 zojt+wgWA0Q(k3shjAFw=KlBditY0Y6+GnjmXScl?XNj65#V9@lJrZVZ#8mHsq-GzK zag}=qubok8EIbs=%8=m%WY`)zS^0=aS*V1LCuDkt;Jmx`Kn7;5JB8j0ek8niYd%>> zsl1tYYt)+6HTL)(u~Hcg!^k)fN8`pPt(d3OnK;@+VzSSv&S#YDk*;}%2_fOf$5vd1 zz=;@Q7=N@u{;^xWuW_T^DyDpB+(oo>eObY(^c{_b?-DzD_|iMs5f1K2hRHUZ({6td z&tn~&w&eyk_x8Mv1h6L*9gk^U6W7_VC5&=k0;+NXAOMh+eMS^*2c>Qv^qB8CCrW}q zv~*|ppNSbA2j|2;XW1-QbX*2Ra z0cHgy8xP);t=T7W-FkSj|CVHpH#_-5Gz~V9iQ+sc$p~l&;8OaVRk6L9nVTl(roJ>y z_XG?|x$y834iK|E)Le@S_lzC!T2Xg{O{S|ajwG&K)-+~#ugdMy^>m>^!VN^q4k*TvgBY&2OcH<9ywf}aousjEpywsZlJ0cRIP@% zX!nkGYTyaTi2;EzqESFYOp=+#q;(-Lz1L*_`7;+D;w=`vX46Vk$@Hha9R>LcfolX(tu~}P{_dK4&aIN(9=p6$aiz|?O505qk zo^GM%(jMvAP&xCdi?N>@hL^$-c`uzjS8<>zb`U;E9a+gW{6Sg z)~tPvS*4|Dmlw z{k=0gLn)SJ8bfyWa>eyX&z&1p<{Jc;q31hg3pZI~%?Cs zn=}E41q#Hy{R>WH4c$vTzm}dA-j)|@0$onX=aAqRE4vZ+tSgoaZDFFVgCA%UkL~z6 z)&@;eCFAR>9_mp1)`yz{wre22&N|+9&yNJ-yxU;qZ_qB< zZX|X!Yjw>g2R8p2k+lMw=kq-ve7zk6)q4s|xRHv}mqYDSHrld;R`zx`zLx1N*m0eA z$)}P!=W4u7@zr$2q6wPPImS%KoQ(U;FXgW`SnTL!EoN_w+yPg3L=poD&zuM3Z6YLLRcV%sr;A|e)e*Jr5o5e#(+DsooDr`B_x`pT#k9i)~W z?OuiSyIe>vPBD_5>{d}_d4rNy(BsM1(m3#!(_Lbf7+&8BMmp)vfYQWBd zBah*7uE6g4ED#NykC;ok9DZLQLZe)PvDXDGQ8TYnvoHvM!J)|o^KlMqDwX1Wfc021 zDM)F}$s@yeaa+03*OPOkF%9J@VbCvOuYFd8R@*N;JSA1Jvh3g-{cry18tR*Lt&8H# zQle%HD7AM{a~?#!R(X{z(v*rw{swxky@fk`@GR_OkGEun`=BOCD=S{BmVy>IR?AO2 zr^HX@DljLPQNnHy@>fm%Sbs>BPtPl@@_O##5&46~<1Qj-R@z!0gr*SKYOV-tZGPZ_({s!Nj+7z+9Fo%N_nW@{ysu}aT4xcIB z&R<+zR*zl2d?L2VR7{usVE`_*;Syirz17m6}9#oLdQ4NXmpp|z9NeQ$;X!%(g( zcMWQ`N>1EX)aoY9ZcVipB))n7w&k2N{#Hf#JnM4#X~Cedvl~?OlEh8^4JGZ`Y81}+hfZhB=cSW?j&=0?s0=*onJ*wUVg^VD1R^ILA!BXd$Yq|;&q&ZhAUs@v z<%V-iRy#PY0IGW@N%Fm6{Br|a_Nk*@$LK9{?q68DNGnMmB~oRL2@`K2c*oeeFj7$! zB5^gD0kjCI^Wi|uylQ8!YjON5N)3qWj`u%#EO&-KEgtYP>BH+jJ2R9wM8DC>hQOS> z?sSBVw^yn_aNHLpBl8TFk_0Au%b2XJWmhdZD5+0}q1I{&E~Eu6P-ovfn{TzHx-E(qo0KfOp7YSIIsO#3nX%26DZlm(cZT z<_}2Tp;Ws_A$OcWaLYmpb9JMp5i-MhP&yhO*wRt&ke6aq+Aqx1gGZgrrSVfr6%yzE z0#xq-3U<%dh4!E1$?+?xiN=l;dB{2C61pb;^0O+Ce2?^M(W>(SMihhO1d+}QXOXA{ zcI17gIe&?R0qey6Jv_wR872!PD$*{}$5FRB3M<>M%!4yE{+YU~D&q;dECL%ob2O?| zYjBXe{Vh=f9o7DeT77x4-_wY%Q+4LRwW*RO;GXRTyhCazOKE&0ok!!;< zMj*Jp@}|=tO!?39bRlayNr1T{!3*;Ck?6_@tf@B}%aq8jUyKm| ze~Qsa_}-9tw2N+aGe`*osw*>(cpmajR2Z@Cq6_=9JGh!$NY*@`RTG7+oZ-(5U{hcq zP=Wf26J)ONC1{0R$XBl~@&3uX@*EAF-R00P-DCJe)bOcBBgkD#t8gIrO>CS4J38vW zpi#Q$7k~M0IJ>){>lefTkf#SX@+VUfH%Nrc)9>;X4Iz$Tio`cmuqN7 zkT7;6i;Fp5IH8aBV|}j!S1U1R^)-~P=gr0(Cm0lJ9r)hkAPLg5fRuPa%Q>l)X!?L- zcurUZWBO;`-L9KL#Z>o6xL6Y@BCn#cM_O+5wO@g^SsF7@7=!H<>?1va@~>0N={^rQ ztbMq30dN(d*rY;oMq4gj55Ad>3kiA;Et%V0pj!$D;!sDGg$?|pO~115_%-97f0vM^ zNahMTU(h4;x^k|^pyh`C&=t6)VpdHH6Q2+Q$uA_IqW8L~9O)fueQ)mnOB^(8G6)WY zu>oOs;Fh8Nh^8exw(J4yj~-$0LbBx2wG+eekKyBz#-$P1NJ1CN`_J3ut6NCU2EQ|f zTZZ)`!fg}&Y*`5K9Z%qEY8Cy6+HvvZs7h?v`D=d`{L4!`ujRUcH{VvH1W<~ zMb419;)+b&tR9OUxG4AvQM6$3lKEk*OM;S;@-R;tK4Dj%^NW3wst{v!l~!ghyN*MW zlvL$cvdv>vPlxK;CpQ#)d0}4KugTmKTRaCMEeey|V6Zh?IHQ@m&{}SXjEXERd%6B+3L5yIb%RaOCO$p+4j-@09TY(OaK4? literal 0 HcmV?d00001 diff --git a/docs/GUIA-USUARIO.md b/docs/GUIA-USUARIO.md new file mode 100644 index 0000000..c989e09 --- /dev/null +++ b/docs/GUIA-USUARIO.md @@ -0,0 +1,272 @@ +# Guia do Usuario - BI Agentes + +Manual de uso do dashboard de transacoes para agentes CambioReal. + +--- + +## Indice + +1. [Acessando o Sistema](#acessando-o-sistema) +2. [Visao Geral do Dashboard](#visao-geral-do-dashboard) +3. [Usando os Filtros](#usando-os-filtros) +4. [Entendendo os KPIs](#entendendo-os-kpis) +5. [Interpretando os Graficos](#interpretando-os-graficos) +6. [Tabela de Transacoes](#tabela-de-transacoes) +7. [Perguntas Frequentes](#perguntas-frequentes) + +--- + +## Acessando o Sistema + +### Login + +1. Abra o navegador e acesse o endereco fornecido pelo administrador +2. Na tela de login, digite: + - **E-mail**: Seu email cadastrado + - **Senha**: Sua senha pessoal +3. Clique em **Entrar** + +``` +┌─────────────────────────────────┐ +│ BI Agentes │ +│ CambioReal - Dashboard │ +│ │ +│ E-mail: [_________________] │ +│ │ +│ Senha: [_________________] │ +│ │ +│ [ Entrar ] │ +└─────────────────────────────────┘ +``` + +**Dica:** Se aparecer a mensagem "E-mail ou senha incorretos", verifique se digitou corretamente. Caso persista, contate o administrador. + +### Logout + +Para sair do sistema, clique no botao **Sair** no canto superior direito do dashboard. + +--- + +## Visao Geral do Dashboard + +Apos o login, voce vera o dashboard com suas transacoes: + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ [Nome do Agente] - Agente [ID] [Ao vivo] [Sair] │ +├────────────────────────────────────────────────────────────────────┤ +│ De: [____] Ate: [____] Granulacao: [___] Fluxo: [___] [Aplicar] │ +├────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ +│ │ KPI 1 │ │ KPI 2 │ │ KPI 3 │ │ KPI 4 │ <- Indicadores │ +│ └────────┘ └────────┘ └────────┘ └────────┘ │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Grafico 1 │ │ Grafico 2 │ <- Graficos │ +│ └─────────────────┘ └─────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Tabela de Transacoes │ <- Detalhes │ +│ └─────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +**Importante:** Voce ve apenas suas proprias transacoes. Outros agentes nao tem acesso aos seus dados. + +--- + +## Usando os Filtros + +Os filtros ficam na barra abaixo do cabecalho. Use-os para refinar a visualizacao: + +### Periodo (De / Ate) + +Selecione a data inicial e final para ver transacoes em um periodo especifico. + +**Exemplo:** Para ver transacoes de janeiro: +- De: `2024-01-01` +- Ate: `2024-01-31` + +### Granulacao + +Escolha como agrupar os dados nos graficos: + +| Opcao | Uso | +|-------|-----| +| **Dia** | Ver detalhes diarios (periodos curtos) | +| **Mes** | Visao mensal (padrao, recomendado) | +| **Ano** | Comparar anos inteiros | + +### Fluxo + +Filtre por tipo de operacao: + +| Opcao | Significado | +|-------|-------------| +| **Todos** | BRL→USD e USD→BRL juntos | +| **BRL → USD** | Apenas envios (reais para dolares) | +| **USD → BRL** | Apenas recebimentos (dolares para reais) | + +### Cliente + +Selecione um cliente especifico ou deixe em "Todos" para ver todas as transacoes. + +### Aplicar + +**Importante:** Apos alterar os filtros, clique em **Aplicar** para atualizar a tela. + +--- + +## Entendendo os KPIs + +Os KPIs (Indicadores-Chave) mostram um resumo do periodo selecionado: + +### Transacoes +Quantidade total de operacoes realizadas. + +### Volume BRL +Soma de todos os valores em reais movimentados. + +**Exemplo:** Se teve 3 transacoes de R$ 10.000, R$ 20.000 e R$ 15.000, o volume e R$ 45.000. + +### Volume USD +Soma de todos os valores em dolares movimentados. + +### Taxa Media +Media ponderada da taxa de cambio cobrada (BRL/USD). + +**Ponderada significa:** Transacoes maiores tem mais peso no calculo. + +### Spread Medio +Porcentagem media de lucro sobre a taxa PTAX. + +**O que e PTAX?** Taxa de cambio oficial do Banco Central. + +**Exemplo:** Se a PTAX e 5,00 e voce cobra 5,05, o spread e 1% (0,05 / 5,00). + +### IOF Total +Soma do IOF (Imposto sobre Operacoes Financeiras) recolhido. + +### Ticket Medio +Valor medio em USD por operacao. + +**Calculo:** Volume USD / Numero de Transacoes + +### Clientes Ativos +Quantos clientes diferentes fizeram transacoes no periodo. + +--- + +## Interpretando os Graficos + +### Volume BRL / USD por Periodo + +Grafico de barras mostrando a evolucao do volume ao longo do tempo. + +- **Barras verdes** = Volume em BRL (eixo esquerdo) +- **Barras azuis** = Volume em USD (eixo direito) + +**Como ler:** +- Barras mais altas = mais volume naquele periodo +- Compare meses para identificar sazonalidade + +### Volume por Cliente (Top 10) + +Grafico horizontal mostrando os 10 maiores clientes por volume USD. + +**Como usar:** +- Identifique seus clientes mais importantes +- Foque atencao nos maiores volumes + +### Taxa Cobrada vs PTAX + +Grafico de linha comparando sua taxa com a taxa oficial. + +- **Linha roxa** = Sua taxa cobrada (media ponderada) +- **Linha azul** = PTAX oficial + +**O que observar:** +- A distancia entre as linhas e seu spread +- Quanto maior a distancia, maior sua margem + +--- + +## Tabela de Transacoes + +A tabela mostra todas as transacoes individuais do periodo. + +### Colunas + +| Coluna | Descricao | +|--------|-----------| +| **Data/Hora** | Quando a transacao foi realizada | +| **Cliente** | Nome do cliente | +| **Valor BRL** | Valor em reais | +| **Valor USD** | Valor em dolares | +| **IOF %** | Aliquota de IOF aplicada | +| **IOF R$** | Valor do IOF em reais | +| **PTAX** | Taxa oficial do BC no momento | +| **Taxa Cobrada** | Sua taxa de cambio | +| **Spread** | Diferenca entre taxa cobrada e PTAX | +| **Spread %** | Spread em porcentagem | +| **Status** | Situacao da operacao | + +### Organizacao + +As transacoes sao separadas por fluxo: +- **BRL → USD** (fundo roxo claro) - Envios +- **USD → BRL** (fundo azul claro) - Recebimentos + +### Navegando + +- Use a barra de rolagem horizontal para ver todas as colunas +- A tabela ordena por data (mais antigas primeiro) + +--- + +## Perguntas Frequentes + +### Posso ver transacoes de outros agentes? + +**Nao.** Cada agente ve apenas suas proprias transacoes. O sistema garante isolamento total dos dados. + +### Os dados sao atualizados em tempo real? + +**Sim.** A cada vez que voce carrega o dashboard ou aplica filtros, os dados sao buscados diretamente do sistema. + +### O que fazer se os graficos nao aparecerem? + +1. Verifique sua conexao com a internet (os graficos usam biblioteca externa) +2. Tente recarregar a pagina (F5) +3. Se persistir, contate o suporte tecnico + +### Como exportar os dados? + +*Em breve!* Estamos desenvolvendo funcionalidade de exportacao para Excel/CSV. + +### A sessao expira? + +**Sim.** Apos 8 horas de inatividade, voce precisara fazer login novamente. + +### Posso acessar de celular? + +**Sim.** O dashboard e responsivo e funciona em dispositivos moveis, porem a experiencia e melhor em telas maiores. + +### O que significa PTAX? + +PTAX e a taxa de cambio oficial divulgada pelo Banco Central do Brasil. E usada como referencia para calcular o spread das operacoes. + +--- + +## Suporte + +Em caso de problemas: +1. Verifique usuario e senha +2. Tente recarregar a pagina +3. Contate o administrador do sistema + +--- + +*CambioReal - BI Agentes v1.0* diff --git a/docs/logo-small.png b/docs/logo-small.png new file mode 100644 index 0000000000000000000000000000000000000000..79f6b47e52436c134841599abdda62bbf39d0973 GIT binary patch literal 4914 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 literal 0 HcmV?d00001 diff --git a/scripts/seed-admin.js b/scripts/seed-admin.js new file mode 100644 index 0000000..b430c06 --- /dev/null +++ b/scripts/seed-admin.js @@ -0,0 +1,70 @@ +/** + * CLI para cadastrar administradores no SQLite + * + * Uso: + * node scripts/seed-admin.js --email admin@cambioreal.com --senha 123456 --nome "Admin" + * + * Listar admins: + * node scripts/seed-admin.js --list + */ +require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); + +const db = require('../src/db-local'); +const { createAdmin } = require('../src/admin-auth'); + +const args = process.argv.slice(2); + +function getArg(name) { + const idx = args.indexOf('--' + name); + return idx !== -1 && args[idx + 1] ? args[idx + 1] : null; +} + +async function main() { + // List mode + if (args.includes('--list')) { + const rows = db.prepare('SELECT id, email, nome, created_at FROM admins').all(); + if (!rows.length) { + console.log('Nenhum administrador cadastrado.'); + } else { + console.log('\n ID | Nome | Email | Criado em'); + console.log(' ' + '-'.repeat(75)); + rows.forEach(r => { + console.log(` ${String(r.id).padEnd(3)}| ${r.nome.padEnd(19)}| ${r.email.padEnd(29)}| ${r.created_at}`); + }); + } + console.log(); + process.exit(0); + } + + // Create mode + const email = getArg('email'); + const senha = getArg('senha'); + const nome = getArg('nome'); + + if (!email || !senha || !nome) { + console.log(` + Uso: node scripts/seed-admin.js --email --senha --nome "" + + Exemplos: + node scripts/seed-admin.js --email admin@cambioreal.com --senha 123456 --nome "Admin" + node scripts/seed-admin.js --list + `); + process.exit(1); + } + + try { + await createAdmin(email, senha, nome); + console.log(`\n Administrador cadastrado com sucesso!`); + console.log(` Nome: ${nome}`); + console.log(` Email: ${email}\n`); + } catch (err) { + if (err.message.includes('UNIQUE')) { + console.error(`\n Erro: email "${email}" ja esta cadastrado.\n`); + } else { + console.error('Erro:', err.message); + } + process.exit(1); + } +} + +main(); diff --git a/src/queries.js b/src/queries.js index 6d37011..198a360 100644 --- a/src/queries.js +++ b/src/queries.js @@ -94,4 +94,133 @@ function serialize(rowsBrlUsd, rowsUsdBrl) { return [...dataBrlUsd, ...dataUsdBrl].sort((a, b) => a.data_sort.localeCompare(b.data_sort)); } -module.exports = { fetchTransacoes, serialize }; +// Fetch ALL transactions (for admin) - with date filter for performance +async function fetchAllTransacoes(diasAtras = 90) { + const conn = await pool.getConnection(); + try { + const [rowsBrlUsd] = await conn.execute(` + SELECT + c.nome AS cliente, + t.created_at AS data_operacao, + t.amount_brl AS valor_reais, + t.amount_usd AS valor_dolar, + t.iof AS iof_pct, + ROUND(t.iof / 100 * t.amount_usd * t.exchange_rate, 2) AS iof_valor_rs, + ROUND(t.ptax, 4) AS taxa_ptax, + ROUND(t.exchange_rate, 4) AS taxa_cobrada, + ROUND(t.exchange_rate - t.ptax, 4) AS spread_bruto, + ROUND((t.exchange_rate - t.ptax) / t.exchange_rate * 100, 2) AS spread_pct, + t.status + FROM br_transaction_to_usa t + INNER JOIN conta c ON c.id_conta = t.id_conta + WHERE t.created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + ORDER BY t.created_at DESC + `, [diasAtras]); + + const [rowsUsdBrl] = await conn.execute(` + SELECT + c.nome AS cliente, + p.created_at AS data_operacao, + p.valor_sol AS valor_reais, + p.valor AS valor_dolar, + 0 AS iof_pct, + 0 AS iof_valor_rs, + ROUND(p.ptax, 4) AS taxa_ptax, + ROUND(p.cotacao, 4) AS taxa_cobrada, + ROUND(p.ptax - p.cotacao, 4) AS spread_bruto, + CASE WHEN p.cotacao > 0 THEN ROUND((p.ptax - p.cotacao) / p.ptax * 100, 2) ELSE 0 END AS spread_pct, + p.pgto AS status + FROM pagamento_br p + INNER JOIN conta c ON c.id_conta = p.id_conta + WHERE p.created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + ORDER BY p.created_at DESC + `, [diasAtras]); + + return { rowsBrlUsd, rowsUsdBrl }; + } finally { + conn.release(); + } +} + +// Fast daily stats for admin home (today and yesterday only) +// Separates USD->BRL (with cotacao) from USD->USD (balance/no cotacao) +async function fetchDailyStats() { + const conn = await pool.getConnection(); + try { + // BRL -> USD counts and volumes for today and yesterday + const [brlUsdStats] = await conn.execute(` + SELECT + DATE(created_at) as dia, + COUNT(*) as qtd, + ROUND(SUM(amount_brl), 2) as total_brl, + ROUND(SUM(amount_usd), 2) as total_usd + FROM br_transaction_to_usa + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) + GROUP BY DATE(created_at) + ORDER BY dia DESC + `); + + // USD -> BRL (with cotacao - real currency exchange) + const [usdBrlStats] = await conn.execute(` + SELECT + DATE(created_at) as dia, + COUNT(*) as qtd, + ROUND(SUM(valor_sol), 2) as total_brl, + ROUND(SUM(valor), 2) as total_usd + FROM pagamento_br + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) + AND cotacao IS NOT NULL AND cotacao > 0 + AND (pgto IS NULL OR pgto != 'balance') + GROUP BY DATE(created_at) + ORDER BY dia DESC + `); + + // USD -> USD (balance or no cotacao - dollar to dollar) + const [usdUsdStats] = await conn.execute(` + SELECT + DATE(created_at) as dia, + COUNT(*) as qtd, + ROUND(SUM(valor), 2) as total_usd + FROM pagamento_br + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) + AND (cotacao IS NULL OR cotacao = 0 OR pgto = 'balance') + GROUP BY DATE(created_at) + ORDER BY dia DESC + `); + + // Format results + const today = new Date().toISOString().slice(0, 10); + const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10); + + const formatDay = (stats, targetDate) => { + const row = stats.find(r => { + const d = r.dia instanceof Date ? r.dia.toISOString().slice(0, 10) : String(r.dia).slice(0, 10); + return d === targetDate; + }); + return row ? { + qtd: Number(row.qtd), + total_brl: Number(row.total_brl) || 0, + total_usd: Number(row.total_usd) || 0 + } : { qtd: 0, total_brl: 0, total_usd: 0 }; + }; + + return { + brlUsd: { + hoje: formatDay(brlUsdStats, today), + ontem: formatDay(brlUsdStats, yesterday) + }, + usdBrl: { + hoje: formatDay(usdBrlStats, today), + ontem: formatDay(usdBrlStats, yesterday) + }, + usdUsd: { + hoje: formatDay(usdUsdStats, today), + ontem: formatDay(usdUsdStats, yesterday) + } + }; + } finally { + conn.release(); + } +} + +module.exports = { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats };