
Capivara — updates e próximos passos
Contexto
O Capivara nasceu como um hub pessoal — um lugar centralizado pra gerenciar meus projetos, acessar dashboards, monitorar saúde da infra e, aos poucos, virou o centro de operações de tudo que eu construo. FastAPI + React, SQLite, Cloudflare Tunnel, JWT.
Nas últimas semanas o foco foi enxugar, organizar e fortalecer. O Capivara tinha crescido desordenadamente — componentes inline de centenas de linhas, rotas misturadas, proxies que não eram mais necessários. Era hora de refatorar.
O que rolou
A grande refatoração do frontend (−83%)
O marco mais pesado foi pegar um frontend de 2.085 linhas e reduzir pra 359 — uma redução de 83% sem perder uma feature sequer.
// Dashboard.tsx: 994 → 262 linhas (−74%)
// AdminPage.tsx: 1.091 → 97 linhas (−91%)
O segredo? Extrair componentes inline. Cada aba do Admin Panel era uma função de 100-200 linhas dentro da própria página. Dashboard tinha 8 seções inline. Separei tudo em components/dashboard/ e components/admin/ — cada componente virou um arquivo dedicado com sua própria responsabilidade.
// Antes: tudo inline no AdminPage.tsx
function OverviewSection() { /* 150 linhas aqui */ }
function LogsSection() { /* 120 linhas aqui */ }
// 10 seções no mesmo arquivo → 1.091 linhas
// Depois: orquestrador puro
import { OverviewSection } from '../components/admin/OverviewSection';
import { LogsSection } from '../components/admin/LogsSection';
// AdminPage vira 97 linhas de imports + layout
Ghost Component Pattern
Durante a refatoração descobri um padrão perigoso que chamei de Ghost Component: componentes existiam como arquivos em components/ mas as páginas ainda tinham definições inline duplicadas. O código funcionava (a versão inline executava), mas ninguém sabia qual versão era a verdadeira.
# Diagnóstico: comparar arquivos exportados vs definições inline
grep -c 'export function' components/*.tsx
grep -c '^function ' pages/*.tsx
A causa é simples: alguém começa a extrair um componente (cria o arquivo), copia o código, mas esquece de remover o original da página. O arquivo vira “morto-vivo” — existe, é importado em lugar nenhum, e a página tem duas versões que podem divergir.
A lição: refatoração incompleta é pior que nenhuma refatoração. Se vai extrair, extraia completo — crie o arquivo, importe na página, remova o inline, teste.
React Router v7 — o fim do window.location caseiro
Antes eu usava um sistema caseiro de roteamento com window.location e listeners manuais. Funcionava, mas era frágil — qualquer navegação SPA exigia sincronização manual de estado.
// Antes: caseiro e frágil
function navigate(path: string) {
window.history.pushState({}, '', path);
window.dispatchEvent(new PopStateEvent('popstate'));
}
// Depois: React Router v7 com BrowserRouter
<BrowserRouter>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/login" element={<Login />} />
<Route path="/cadastro" element={<Register />} />
<Route path="/status" element={<StatusPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
6 rotas, navegação SPA real, sem reload, sem gambiarra. React Router cuida de tudo — match de URL, history, link components.
Cada página é carregada com React.lazy() + Suspense, então o bundle inicial caiu de 668 kB pra 238 kB — uma redução de 64% no que o usuário baixa no primeiro acesso.
const Dashboard = lazy(() => import('./pages/Dashboard'))
const AdminPage = lazy(() => import('./pages/AdminPage'))
const StatusPage = lazy(() => import('./pages/StatusPage'))
Schema Drift — o bug silencioso que o security scan pegou
O painel admin consulta dados do Arachne lendo diretamente o SQLite dele. O problema: Capivara e Arachne são projetos independentes, e o schema do Arachne muda sem o Capivara saber.
# O erro silencioso
def _query_sqlite(db_path, sql, params):
try:
conn = sqlite3.connect(str(db_path))
cur = conn.execute(sql, params)
return [dict(row) for row in cur.fetchall()]
except Exception as e:
log.warning("SQLite query failed: %s", e)
return [] # ← silencioso!
O security scan detectou colunas que não existiam mais. O pior: o erro era capturado e retornava [] — a seção Arachne no admin simplesmente aparecia vazia, sem crash, sem feedback visual.
Criei um schema drift guard (scripts/schema_drift_guard.sh) que calcula hash SHA256 das definições SQLAlchemy e compara com um hash armazenado:
# O coração do schema_drift_guard.sh
SCHEMA_HASH=$(python3 -c "
import hashlib
from database import Base
import models
lines = []
for table_name in sorted(Base.metadata.tables.keys()):
table = Base.metadata.tables[table_name]
lines.append(f'TABLE: {table_name}')
for col_name, col in sorted(table.columns.items()):
col_repr = f' {col_name}: {col.type!r} nullable={col.nullable}'
if col.primary_key: col_repr += ' PK'
lines.append(col_repr)
print(hashlib.sha256('\\n'.join(lines).encode()).hexdigest())
")
Se o hash mudou, o script alerta e atualiza o hash armazenado. Rodo como cron diário às 06:00.
Portfolio Data API — PostgreSQL 18 como persistence layer
O Portfolio rodando na Vercel agora persiste dados reais via Cloudflare Tunnel → Capivara → PostgreSQL 18 local.
Portfolio (Vercel) → Cloudflare Tunnel → Capivara:8001 → PG18:5432
Três endpoints públicos (sem auth):
POST /api/portifolio/public/messages— formulário de contatoPOST /api/portifolio/public/cv-downloads— registro de download de currículoPOST /api/portifolio/public/events— eventos de monitoramento
O código é direto: Pydantic schemas validam entrada, psycopg2 persiste, e cada endpoint captura IP + User-Agent automaticamente. No dashboard admin, vejo mensagens recebidas, downloads, e eventos de tracking — tudo em tempo real.
O banco é PostgreSQL 18 rodando local — escolhi PG em vez de SQLite porque o Portfolio pode ter concorrência real de visitantes, e PG lida melhor com conexões simultâneas.
ThemeToggle + Toast System
Pequenas melhorias de UX que fazem diferença:
- ThemeToggle: alternador claro/escuro com persistência em
localStorage+ fallback praprefers-color-scheme. O estado persiste entre sessões.
function ThemeToggle() {
const [dark, setDark] = useState(() => {
const stored = localStorage.getItem('capivara_theme')
if (stored) return stored === 'dark'
return window.matchMedia('(prefers-color-scheme: dark)').matches
})
// ...
}
- Toast system: notificações toast em todo o app, integrado ao Root.tsx. Suporta 4 tipos (success, error, info, warning) com fade-out automático em 4 segundos. Feedback visual pra ações como criar convite, salvar configuração.
// Uso em qualquer lugar do app
import { toast } from '../components/common/Toast'
toast('Convite criado com sucesso!', 'success')
-
StatusPage: página pública em
/statusque mostra health check dos serviços — útil pra compartilhar com terceiros sem dar acesso ao dashboard. Carregada com lazy loading, então não impacta o bundle principal. -
ErrorBoundary: proteção contra crash em qualquer parte da árvore de componentes. Cada rota lazy tem seu próprio boundary, então um erro no admin não derruba o dashboard.
<ErrorBoundary name="Admin">
<Suspense fallback={<PageLoader />}>
<AdminPage />
</Suspense>
</ErrorBoundary>
Aprendizados
1. Refatoração é cirurgia, não demolição
Extrair 18 componentes de 2 páginas sem quebrar nada exige passos atômicos: extrair um componente de cada vez, testar, commitar, repetir. Tentei fazer 3 de uma vez e quebrou o build do TypeScript. O tsc --noEmit é seu melhor amigo — rode antes e depois de cada extração.
2. Dados de terceiros precisam de validação de schema
Quando você consulta o banco de outro projeto, nunca confie no schema. Use PRAGMA table_info() pra validar as colunas ANTES de fazer SELECT. Melhor ainda: crie um contrato de schema versionado entre os projetos.
// Validar schema antes de consultar
const columns = await db.query("PRAGMA table_info('extractions')");
const hasColumn = columns.some(c => c.name === 'page_id');
if (!hasColumn) throw new Error('Schema mismatch: extractions missing page_id');
3. Componentes inline são dívida técnica disfarçada
Toda função que você escreve inline numa página de 500+ linhas é um componente que vai ser extraído um dia — e quanto mais tarde, mais caro. O padrão “extraio depois” só funciona se você realmente extrai logo depois. Deixar acumular vira uma bola de neve de 2.000 linhas.
4. Sistema de 2 serviços é mais frágil que 2 sistemas independentes
O Capivara consultando o SQLite do Arachne é frágil exatamente porque funciona bem na maior parte do tempo. Quando quebra (schema muda), quebra silenciosamente. APIs HTTP com versionamento são mais caras de implementar, mas muito mais seguras.
O que vem por aí
O Capivara tá num bom lugar agora — enxuto, organizado, com testes passando (44/44 backend, 31/31 frontend) e build limpo (0 erros TypeScript). Os próximos passos:
- 2FA — autenticação de dois fatores pro acesso admin (já tem suporte backend, falta a UI de configuração)
- WebSocket — notificações em tempo real (health alerts, novos convites)
- Mais integração com o Arachne — via API HTTP, não SQL direto
- Dark mode completo — ThemeToggle já existe, mas algumas seções ainda precisam de ajustes finos
- PWA — manifest + service worker pra instalar como app
# O deploy continua simples
cd ~/projetos/Capivara/frontend && pnpm build # 238 kB initial bundle
systemctl --user restart capivara-backend.service # zero downtime
Código enxuto, sistema saudável, próximo feature a caminho. O Capivara cresceu, foi podado e tá mais forte.