feat(infraestrutura): adiciona plugin infraestrutura (agents, skills, knowledge e metadados do projeto)

This commit is contained in:
2026-10-28 16:09:01 +00:00
parent de2d6f2fb5
commit e43fec3bd9
73 changed files with 8589 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# AGENTS.md — skills
Directrizes específicas deste projecto para agentes IA.
## Referências obrigatórias
- `ECOSYSTEM.md` — Hub/06-Operacoes/Documentacao/ECOSYSTEM.md
- `PROC-DEV-STANDARD` — D7-SIS-006
- `PROC-AUDIT-STANDARD` — D7-SIS-007
## Protocolo de sessão
Início: ler MEMORY.md + STATUS.md. Fim: actualizar ambos.
## Convenções específicas
- (a definir)
+5
View File
@@ -0,0 +1,5 @@
# CHANGELOG — skills
## 0.1.0 — 2026-06-30
### Adicionado
- Estrutura padrão (PROC-DEV-STANDARD)
+4
View File
@@ -0,0 +1,4 @@
# MEMORY.md — skills
## 2026-06-30
- Estrutura padrão aplicada via propagate-structure.sh
+3
View File
@@ -0,0 +1,3 @@
# skills
Subprojecto skills do ecossistema Descomplicar.
+8
View File
@@ -0,0 +1,8 @@
# STATUS.md — skills
**Versão:** 0.1 | **Data:** 2026-06-30 | **Agente:** —
## Estado actual
- Em curso: —
- Bloqueios: —
- Próximos passos: —
+164
View File
@@ -0,0 +1,164 @@
---
name: authentik
description: Gestão do Authentik SSO via API v3 — utilizadores, grupos, aplicações, providers, flows e eventos em auth.descomplicar.pt.
disable-model-invocation: true
layer: wiki
---
# /authentik - Gestao Authentik via API v3
Gestao do Authentik SSO (auth.descomplicar.pt) via API REST directa com curl.
## Conexao
- **Base URL:** `https://auth.descomplicar.pt/api/v3`
- **Auth Header:** `Authorization: Bearer Sa4Zofsdm7UoMXrHRAZ4sIsvNIqCtEQ3uCZrsDg7kx3wDCKQqI0frlFuoUVS`
- **Painel Web:** https://auth.descomplicar.pt/if/admin/
## Template Base
Todas as chamadas usam este padrao:
```bash
curl -sk "https://auth.descomplicar.pt/api/v3/{ENDPOINT}" \
-H "Authorization: Bearer Sa4Zofsdm7UoMXrHRAZ4sIsvNIqCtEQ3uCZrsDg7kx3wDCKQqI0frlFuoUVS" \
-H "Content-Type: application/json"
```
Para POST/PUT/PATCH, adicionar `-X {METHOD} -d '{JSON}'`.
## Endpoints Principais
### Core - Users
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar users | GET | `/core/users/` |
| Obter user | GET | `/core/users/{id}/` |
| Criar user | POST | `/core/users/` |
| Actualizar user | PATCH | `/core/users/{id}/` |
| Apagar user | DELETE | `/core/users/{id}/` |
| Pesquisar user | GET | `/core/users/?search={query}` |
| User por username | GET | `/core/users/?username={name}` |
| Desactivar user | PATCH | `/core/users/{id}/` com `{"is_active": false}` |
| Password user | POST | `/core/users/{id}/set_password/` com `{"password": "..."}` |
| Recovery link | POST | `/core/users/{id}/recovery/` |
**Campos user (POST/PATCH):**
```json
{
"username": "nome.apelido",
"name": "Nome Completo",
"email": "email@dominio.pt",
"is_active": true,
"groups": ["uuid-grupo"],
"attributes": {"chave": "valor"}
}
```
### Core - Groups
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar groups | GET | `/core/groups/` |
| Criar group | POST | `/core/groups/` |
| Actualizar group | PATCH | `/core/groups/{id}/` |
| Apagar group | DELETE | `/core/groups/{id}/` |
| Adicionar user a group | POST | `/core/groups/{id}/add_user/` com `{"pk": user_id}` |
| Remover user de group | POST | `/core/groups/{id}/remove_user/` com `{"pk": user_id}` |
### Core - Applications
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar apps | GET | `/core/applications/` |
| Obter app | GET | `/core/applications/{slug}/` |
| Criar app | POST | `/core/applications/` |
| Actualizar app | PATCH | `/core/applications/{slug}/` |
| Apagar app | DELETE | `/core/applications/{slug}/` |
**Campos app:**
```json
{
"name": "Nome App",
"slug": "nome-app",
"provider": provider_pk,
"launch_url": "https://app.dominio.pt",
"open_in_new_tab": true,
"meta_description": "Descricao"
}
```
### Providers (OAuth2/Proxy/LDAP)
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar OAuth2 | GET | `/providers/oauth2/` |
| Criar OAuth2 | POST | `/providers/oauth2/` |
| Listar Proxy | GET | `/providers/proxy/` |
| Criar Proxy | POST | `/providers/proxy/` |
| Listar LDAP | GET | `/providers/ldap/` |
| Listar todos | GET | `/providers/all/` |
**OAuth2 provider campos:**
```json
{
"name": "Nome Provider",
"authorization_flow": "flow-uuid",
"invalidation_flow": "flow-uuid",
"client_type": "confidential",
"redirect_uris": "https://app.dominio.pt/callback"
}
```
### Flows
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar flows | GET | `/flows/instances/` |
| Obter flow | GET | `/flows/instances/{slug}/` |
| Flow bindings | GET | `/flows/bindings/` |
### Events (Audit)
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar events | GET | `/events/events/` |
| Filtrar por accao | GET | `/events/events/?action={action}` |
| Filtrar por user | GET | `/events/events/?username={user}` |
| Notifications | GET | `/events/notifications/` |
| System tasks | GET | `/events/system_tasks/` |
**Accoes comuns:** `login`, `login_failed`, `logout`, `user_write`, `model_created`, `model_updated`, `model_deleted`, `authorize_application`
### Tokens
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar tokens | GET | `/core/tokens/` |
| Criar token | POST | `/core/tokens/` |
| Obter token | GET | `/core/tokens/{identifier}/` |
| Apagar token | DELETE | `/core/tokens/{identifier}/` |
| View key | GET | `/core/tokens/{identifier}/view_key/` |
### Outposts
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| Listar outposts | GET | `/outposts/instances/` |
| Health outpost | GET | `/outposts/instances/{id}/health/` |
| Service connections | GET | `/outposts/service_connections/all/` |
### Admin / Sistema
| Operacao | Metodo | Endpoint |
|----------|--------|----------|
| System info | GET | `/admin/system/` |
| Version | GET | `/admin/version/` |
| Workers | GET | `/admin/workers/` |
| Metrics | GET | `/admin/metrics/` |
| System tasks | GET | `/admin/system_tasks/` |
## Paginacao
Todos os endpoints de lista sup
@@ -0,0 +1,165 @@
---
name: backup-strategies
description: Planeamento e implementacao de estrategias de backup -- bases de dados, ficheiros e configuracoes com RPO/RTO e regra 3-2-1.
layer: wiki
---
# /backup-strategies - Backup e Disaster Recovery
Implementacao de estrategias de backup completas seguindo best practices de RPO/RTO e regra 3-2-1.
---
## Quando usar
- Planear estrategia de backup para novo projecto
- Implementar backup automatizado
- Configurar disaster recovery
- Definir politicas de retencao
- Testar procedimentos de restore
- Auditar backups existentes
- Recuperar de falha/corrupcao
---
## Conceitos core
### RPO vs RTO
```
RPO (Recovery Point Objective)
= Maximo de dados que pode perder
= Frequencia minima de backups
RTO (Recovery Time Objective)
= Tempo maximo para restaurar
= Define tipo de solucao DR
```
**Tabela de decisao:**
| Criticidade | RPO | RTO | Solucao | Custo |
|-------------|-----|-----|---------|-------|
| **Critico** | 0 | <1h | Replicacao real-time + hot standby | Alto |
| **Alto** | 1h | <4h | Backup horario + warm standby | Medio-alto |
| **Medio** | 24h | <24h | Backup diario + cold backup | Medio |
| **Baixo** | 7d | <72h | Backup semanal | Baixo |
### Regra 3-2-1
- **3** copias dos dados (producao + backup local + backup offsite)
- **2** tipos de media diferentes (disco local + cloud)
- **1** copia offsite (protege contra desastres locais)
**Implementacao Descomplicar (estado real — fonte de verdade: `Hub/05-Projectos/SistemaBackups/`):**
1. **Producao:** Servidor CWP (VM 100) + EasyPanel (VM 101) no cluster Proxmox
2. **Backup:** Proxmox vzdump → PBS (6TB) + MySQL hourly + rsync servidores → cluster
3. **Offsite:** **pendente** (regra 3-2-1 em 2-1-0; decisao Hetzner Storage Box / Backblaze B2 / Wasabi). Insync/rclone→GDrive estao mortos — nao assumir como activos.
> Esta skill cobre conceitos genericos (RPO/RTO, GFS, 3-2-1). Para o estado e topologia reais do sistema Descomplicar, consultar sempre `Hub/05-Projectos/SistemaBackups/AUDITORIA-Backups-2026-06-08.md`.
---
## Workflow principal
### Passo 1: Definir RPO/RTO
Classificar cada sistema pela tabela de criticidade acima.
### Passo 2: Implementar scripts
Scripts prontos para MySQL, WordPress, CWP e rotacao GFS:
**-> [references/scripts.md](references/scripts.md)**
Inclui:
- Backup MySQL com `--single-transaction` e compressao
- Backup PostgreSQL com `pg_dump`
- Backup incremental MySQL com binlog
- Backup WordPress completo (com exclusoes inteligentes)
- Backup incremental com rsync
- Backup contas e configuracoes CWP
- Rotacao GFS (Grandfather-Father-Son)
- Cron schedule completo
- Cloud sync com rclone
- Script teste de restore
- Runbook disaster recovery
### Passo 3: Configurar retencao GFS
| Tipo | Retencao | Frequencia | Slots |
|------|----------|------------|-------|
| **Horario** | 24h | A cada hora | 24 |
| **Diario** | 7 dias | 1x/dia (00:00) | 7 |
| **Semanal** | 4 semanas | 1x/semana (domingo) | 4 |
| **Mensal** | 12 meses | 1x/mes (dia 1) | 12 |
| **Anual** | 7 anos | 1x/ano (1 janeiro) | 7 |
**Total:** 54 backups simultaneos
### Passo 4: Automatizar com cron
Ver schedule completo em `references/scripts.md` (seccao Cron Schedule).
### Passo 5: Configurar cloud sync
Configurar rclone para sync automatico. Ver detalhes em `references/scripts.md`.
### Passo 6: Testar restore (mensal obrigatorio)
Executar script de teste de restore mensalmente. Ver script em `references/scripts.md`.
---
## Disaster recovery
### Checklist DR
- [ ] Backups testados (restore funciona?)
- [ ] Documentacao actualizada (runbook)
- [ ] Runbook de recovery acessivel offline
- [ ] Contactos de emergencia definidos
- [ ] Acesso offsite confirmado (credenciais)
- [ ] Teste DR trimestral agendado
- [ ] RTO/RPO documentados
- [ ] Plano de comunicacao (clientes/equipa)
### Runbook
Ver runbook completo passo-a-passo em `references/scripts.md` (seccao Runbook Disaster Recovery).
**RTO total estimado:** 4-6 horas
---
## Anti-patterns
| Anti-pattern | Problema | Solucao |
|--------------|----------|---------|
| Backup sem teste | Pode falhar no restore | Teste mensal obrigatorio |
| Apenas local | Perda em desastre | Regra 3-2-1 (offsite obrigatorio) |
| Sem encriptacao | Dados expostos | Encriptar backups sensiveis |
| Manual | Esquecimento humano | Automatizar via cron |
| Sem alertas | Falhas nao detectadas | Monitorizacao + notificacoes |
| Password no comando | Inseguro (history, ps) | Usar .my.cnf ou variaveis ENV |
| Backup sem rotacao | Disco cheio | Script de rotacao automatico |
| Sem documentacao | Caos em emergencia | Runbook DR actualizado |
---
## Checklist de implementacao
### Planeamento
- [ ] RPO/RTO definidos por sistema
- [ ] Regra 3-2-1 implementada
- [ ] Politica de retencao GFS
- [ ] Budget aprovado (storage, cloud)
### Automacao
- [ ] Scripts de backup criados
- [ ] Cron jobs configurados
- [ ] Rotacao automatica activa
- [ ] Cloud sync activo (rclone)
### Seguranca
- [ ] Encriptacao em backups se
@@ -0,0 +1,297 @@
# /backup-strategies - Scripts e Automação
Scripts prontos a usar para backup e disaster recovery.
## MySQL - Dump com Compressão
```bash
#!/bin/bash
# backup-mysql.sh
DB_NAME="ealmeida_desk24"
DB_USER="root"
DB_PASS="PASSWORD" # Usar ficheiro .my.cnf na prática
BACKUP_DIR="/backups/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
mkdir -p "$BACKUP_DIR"
mysqldump \
--single-transaction \
--routines \
--triggers \
--events \
--hex-blob \
-u "$DB_USER" \
-p"$DB_PASS" \
"$DB_NAME" | gzip -9 > "$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz"
if [ $? -eq 0 ]; then
SIZE=$(du -h "$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz" | cut -f1)
echo "Backup $DB_NAME: $SIZE"
else
echo "ERRO ao fazer backup de $DB_NAME"
exit 1
fi
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +$RETENTION_DAYS -delete
```
**Opções Críticas:**
- `--single-transaction`: Consistência sem lock (InnoDB)
- `--routines`: Incluir stored procedures
- `--hex-blob`: Formato binário seguro para BLOBs
**Anti-Patterns:**
- `mysqldump database > backup.sql` sem `--single-transaction`
- Backup sem compressão
- Password na linha de comando
## PostgreSQL - pg_dump
```bash
# Backup com compressão máxima
pg_dump -Fc -Z9 -d database_name > backup_$(date +%Y%m%d_%H%M%S).dump
# Backup de cluster completo
pg_dumpall -c | gzip > cluster_backup_$(date +%Y%m%d).sql.gz
```
## Backup Incremental MySQL com Binlog
```bash
# Activar binlog (my.cnf):
# [mysqld]
# log-bin=/var/log/mysql/mysql-bin.log
# expire_logs_days=7
mysqlbinlog --read-from-remote-server --host=localhost \
--start-datetime="2026-02-03 00:00:00" \
mysql-bin.000001 > incremental_backup.sql
```
---
## Backup WordPress
### Backup Completo
```bash
#!/bin/bash
# backup-wordpress.sh
SITE_NAME="emanuelalmeida"
SITE_PATH="/home/ealmeida/emanuelalmeida.pt"
BACKUP_DIR="/backups/wordpress"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/${SITE_NAME}_${DATE}.tar.gz" \
--exclude='wp-content/cache' \
--exclude='wp-content/uploads/cache' \
--exclude='wp-content/upgrade' \
--exclude='*.log' \
-C "$(dirname $SITE_PATH)" \
"$(basename $SITE_PATH)"
if [ -f "$BACKUP_DIR/${SITE_NAME}_${DATE}.tar.gz" ]; then
tar -tzf "$BACKUP_DIR/${SITE_NAME}_${DATE}.tar.gz" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "Backup OK e verificado"
else
echo "Arquivo corrompido!"
exit 1
fi
fi
find "$BACKUP_DIR" -name "${SITE_NAME}_*.tar.gz" -mtime +7 -delete
```
### Backup Incremental com rsync
```bash
#!/bin/bash
SOURCE="/home/ealmeida/emanuelalmeida.pt"
BACKUP_BASE="/backups/incremental"
DATE=$(date +%Y%m%d)
rsync -avz --delete \
--backup --backup-dir="$BACKUP_BASE/delta/$DATE" \
--exclude='wp-content/cache' \
"$SOURCE/" "$BACKUP_BASE/current/"
```
---
## Backup Servidor CWP
### Backup Conta CWP
```bash
/usr/local/cwpsrv/htdocs/resources/scripts/backup_accounts \
--account=username \
--type=full \
--destination=/backups/cwp
```
### Backup Configurações Servidor
```bash
#!/bin/bash
BACKUP_DIR="/backups/config"
DATE=$(date +%Y%m%d_%H%M%S)
tar -czf "$BACKUP_DIR/server_config_${DATE}.tar.gz" \
/etc/nginx \
/etc/apache2 \
/etc/mysql \
/etc/my.cnf \
/etc/php* \
/usr/local/cwpsrv/conf \
/root/.acme.sh/cwp_certs \
/etc/cron* \
/etc/fstab \
/etc/hosts
```
---
## Rotação GFS (Grandfather-Father-Son)
```bash
#!/bin/bash
BACKUP_DIR="/backups"
HOURLY_KEEP=24
DAILY_KEEP=7
WEEKLY_KEEP=4
MONTHLY_KEEP=12
find "$BACKUP_DIR/hourly" -mtime +1 -delete
find "$BACKUP_DIR/daily" -mtime +$DAILY_KEEP -delete
find "$BACKUP_DIR/weekly" -mtime +$((WEEKLY_KEEP * 7)) -delete
find "$BACKUP_DIR/monthly" -mtime +$((MONTHLY_KEEP * 30)) -delete
du -sh "$BACKUP_DIR"/*
```
| Tipo | Retenção | Frequência |
|------|----------|------------|
| Horário | 24h | A cada hora |
| Diário | 7 dias | 1x/dia (00:00) |
| Semanal | 4 semanas | 1x/semana |
| Mensal | 12 meses | 1x/mês |
---
## Cron Schedule
```cron
# Backup MySQL (diário às 02:00)
0 2 * * * /root/scripts/backup-mysql.sh >> /var/log/backup-mysql.log 2>&1
# Backup WordPress (diário às 03:00)
0 3 * * * /root/scripts/backup-wordpress.sh >> /var/log/backup-wp.log 2>&1
# Backup configurações (semanal, domingo 04:00)
0 4 * * 0 /root/scripts/backup-server-config.sh >> /var/log/backup-config.log 2>&1
# Rotação backups (diário às 05:00)
0 5 * * * /root/scripts/backup-rotation-gfs.sh >> /var/log/backup-rotation.log 2>&1
# Teste de restore (mensal, dia 1 às 06:00)
0 6 1 * * /root/scripts/test-restore.sh >> /var/log/test-restore.log 2>&1
# Sync para cloud (diário às 07:00)
0 7 * * * rclone sync /backups gdrive:backups >> /var/log/rclone-sync.log 2>&1
```
---
## Cloud Sync com rclone
```bash
# Instalar rclone
curl https://rclone.org/install.sh | sudo bash
# Configurar Google Drive
rclone config
# Nome: gdrive | Tipo: drive | Seguir wizard
```
```bash
#!/bin/bash
LOCAL_DIR="/backups"
REMOTE="gdrive:backups"
rclone sync "$LOCAL_DIR" "$REMOTE" \
--exclude "*.tmp" \
--exclude "*.log" \
--progress
```
---
## Teste de Restore (OBRIGATÓRIO Mensal)
```bash
#!/bin/bash
TEST_DB="test_restore_$(date +%Y%m%d)"
BACKUP_FILE="/backups/mysql/ealmeida_desk24_latest.sql.gz"
mysql -e "CREATE DATABASE $TEST_DB;"
gunzip < "$BACKUP_FILE" | mysql "$TEST_DB"
TABLES=$(mysql -Nse "SELECT COUNT(*) FROM information_schema.TABLES WHERE table_schema='$TEST_DB';")
echo "Tabelas restauradas: $TABLES"
mysqlcheck --check "$TEST_DB" | grep -i error
mysql -e "DROP DATABASE $TEST_DB;"
echo "Teste concluído"
```
---
## Runbook Disaster Recovery
```markdown
# DISASTER RECOVERY RUNBOOK
## Cenário: Servidor CWP Inoperacional
### Passo 1: Avaliação (5 min)
- [ ] Confirmar que servidor não responde (ping, SSH)
- [ ] Identificar tipo de falha (hardware, software, ataque)
- [ ] Comunicar clientes afectados (se downtime >1h)
### Passo 2: Provisionar Servidor (30 min)
- [ ] Ubuntu 22.04 ou CentOS 7
- [ ] Instalar CWP: `sh cwp-el7-latest.sh`
- [ ] Configurar firewall e SSH
### Passo 3: Restore Bases de Dados (1-2h)
```bash
gunzip ealmeida_desk24_20260203_120000.sql.gz
mysql -e "CREATE DATABASE ealmeida_desk24;"
mysql ealmeida_desk24 < ealmeida_desk24_20260203_120000.sql
```
### Passo 4: Restore Ficheiros (1-2h)
```bash
tar -xzf wordpress_sites_20260203.tar.gz -C /tmp/
cp -r /tmp/emanuelalmeida.pt /home/ealmeida/
chown -R ealmeida:ealmeida /home/ealmeida/emanuelalmeida.pt
```
### Passo 5: Validação (30 min)
- [ ] Testar acesso SSH
- [ ] MySQL a responder
- [ ] Sites WordPress a carregar
- [ ] SSL activo
**RTO Total Estimado:** 4-6 horas
```
+106
View File
@@ -0,0 +1,106 @@
---
name: cwp
description: Meta-skill CWP (CentOS Web Panel, servidor 176.9.3.158) cobrindo gestão de contas, backups, email/Postfix/DKIM, PHP Switcher/Selector, segurança CSF/LFD, SSL/acme.sh, webservers Apache/Nginx e referência completa dos scripts oficiais. Triggers — cwp, centos web panel, cwp_api, /scripts/, csf, lfd, acme, autossl, postfix, dkim, php selector, php switcher, varnish, httpd, nginx cwp, user_backup, whoowns, list_users, hostname ssl, mail queue, fix_perms.
layer: wiki
---
# /cwp - Meta-skill CWP (CentOS Web Panel)
Skill unificada para o servidor **CWP Principal `176.9.3.158`** (MCP `ssh-unified`, user `root`). Substitui as antigas cwp-accounts, cwp-backup, cwp-email, cwp-php, cwp-scripts, cwp-security, cwp-ssl, cwp-webserver.
**REGRA #38 — ZERO INVENÇÃO:** NUNCA improvisar comandos. Verificar estado actual ANTES de alterar. Pesquisar wiki CWP se necessário. Usar APENAS scripts oficiais (`/scripts/*` e `/scripts/cwp_api`). Se não souber: PERGUNTAR.
---
## 0. GATE OBRIGATÓRIO — checklist pré-intervenção CWP
> **Antes de QUALQUER alteração em CWP (SSL, email/Postfix/Dovecot, nginx/Apache, CSF, PHP), correr este checklist. Padrão recorrente de edições manuais desnecessárias = causa de incidentes repetidos (#3992).**
1. **CONSULTAR PRIMEIRO** (Regra 53 — LLM Wiki obrigatório):
- `PROC-CWP-SSL.md` (D7 Infraestrutura) — para SSL/certificados.
- `PROC-Email-Antispam-CWP.md` (D7 Infraestrutura) — para email/Postfix/DKIM/antispam.
- NotebookLM CWP `0ded7bd6-69b3-4c76-b327-452396bf7ea7` + `~/wiki/` se o caso não estiver nos PROCs.
2. **VERIFICAR ESTADO ACTUAL** (read-only) antes de tocar — ver secção relevante abaixo (ex.: `openssl s_client` para SSL, `postqueue -p` para email).
3. **SCRIPT NATIVO, NUNCA EDIÇÃO MANUAL.** Se existe `/scripts/*` para a tarefa, usar esse. Editar Postfix/Dovecot/vhosts/certs à mão é anti-pattern (sobrescrito em updates + causa-raiz de drift).
- **SSL de email partido → `sh /scripts/mail_rebuild_sni_certs`** (NÃO editar `main.cf`/`master.cf` manualmente).
- Certs web → acme.sh nativo (§6). Webservers → `cwp_api webservers rebuild_*` (§7). Permissões pós-SSH → `cwp_api account fix_perms` (§1).
4. **EMAIL EM PRODUÇÃO = ZERO RISCO:** não mexer em `master.cf`/postscreen nem reiniciar Postfix sem necessidade comprovada. Mudança mínima, testada, reversível.
5. **HDD 4TB — sem comandos cegos:** NUNCA `du`/`find` genéricos em `/home` (bloqueia horas, iowait >30%). Caminhos específicos sempre.
6. **VALIDAR + DOCUMENTAR:** confirmar o efeito (secção "Validação" relevante) e registar o que se descobriu (memória/PROC) se for novo.
> Se os passos 1-3 não resolvem e seria preciso editar à mão: **PARAR e perguntar**. A edição manual é o último recurso, não o primeiro.
## Documentação base
- Wiki: https://wiki.centos-webpanel.com/cwp-admin-api , /cwp-scripts , /csf-firewall-command-line
- Hub manuais: `Hub/06-Operacoes/Documentacao/Manuais/CWP/` (`CWP-Manual-Completo.md` 503KB, `CWP-Ferramentas-Desenvolvimento.md` 82KB, `CWP-Guia-do-Utilizador.md`, `CWP-Guia-do-Revendedor.md`)
- QR: `Hub/06-Operacoes/Documentacao/Quick-Reference/QR-CWP.md`
- NotebookLM CWP: `0ded7bd6-69b3-4c76-b327-452396bf7ea7`
- NotebookLM Cibersegurança WordPress (para secção segurança): `5f60adfd-2435-4725-8c12-9c11c5f51d75`
```
mcp__notebooklm__notebook_query notebook_id:"0ded7bd6-69b3-4c76-b327-452396bf7ea7" query:"<adaptar>"
```
---
## 1. Contas (accounts)
### Consulta (read-only)
```bash
sh /scripts/list_users
sh /scripts/list_domains
sh /scripts/list_subdomains
sh /scripts/whoowns DOMINIO
```
### API gestão
```bash
/scripts/cwp_api account remove_user USERNAME
/scripts/cwp_api account suspend_user USERNAME
/scripts/cwp_api account unsuspend_user USERNAME
/scripts/cwp_api account fix_perms USERNAME # OBRIGATÓRIO após qualquer SSH manual
/scripts/cwp_api account list_domains USERNAME
```
### Bandwidth
```bash
/scripts/cwp_api account unsuspend_bandwidth USERNAME # após aumentar limites
/scripts/cwp_api account reset_bandwidth USERNAME
```
### Massa
```bash
/scripts/cwp_api account update_diskquota_all
/scripts/cwp_api account update_limits_all # quota, hourly_emails, cgroups, nproc, apache_nproc, inode, nofile
/scripts/cwp_api account mail_fix_permissions
/scripts/cwp_api account update_policyd_all
```
### DNS
```bash
/scripts/cwp_api account rebuild_etc_named_conf
/scripts/cwp_api account rebuild_var_named_all
/scripts/cwp_api account rebuild_var_named USERNAME DOMAIN
```
### Auxiliares
```bash
sh /scripts/ulimit_user_check USERNAME
sh /scripts/cwpsrv_rebuild_user_conf
```
### Workflows típicos
1. Suspender: `suspend_user` -> `list_users | grep`.
2. Reactivar: `unsuspend_user` -> `fix_perms`.
3. Pós-SSH manual: SEMPRE `fix_perms USERNAME`.
### Validação
```bash
sh /scripts/list_users | grep USERNAME
/scripts/cwp_api account list_domains USERNAME
curl -sI http://DOMINIO | head -5
```
### Anti-patterns
Remover sem backup | editar ficheiros manualmente | esquecer
+6
View File
@@ -0,0 +1,6 @@
{
"task_id": null,
"project_id": null,
"customer_id": null,
"gitea_repo": null
}
@@ -0,0 +1,139 @@
---
name: easypanel-api
description: Referência completa da API tRPC oficial do EasyPanel — todos os endpoints para projectos, serviços, deploy e configurações.
layer: wiki
---
# /easypanel-api - API Oficial EasyPanel
Referência da API tRPC do EasyPanel. Baseado em engenharia reversa do SDK oficial.
---
## Autenticação
```bash
TOKEN=$(cat /etc/easypanel/.api-token)
# Header obrigatório em todos os pedidos:
-H "Authorization: Bearer $TOKEN"
```
**Base URL:** `http://localhost:3000/api/trpc/`
Usar sempre via SSH localhost. Nunca expor externamente.
---
## Formato de Pedidos
| Método | Quando usar | Formato |
|--------|-------------|---------|
| GET | Leitura (list, inspect, get) | `?input=URL_ENCODED_JSON` |
| POST | Escrita (create, update, deploy, destroy) | `-d '{"json":{...}}'` com `Content-Type: application/json` |
```bash
# GET
curl -s "http://localhost:3000/api/trpc/ENDPOINT?input=URL_ENCODED_JSON" \
-H "Authorization: Bearer $TOKEN"
# POST
curl -s -X POST "http://localhost:3000/api/trpc/ENDPOINT" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{...}}'
```
---
## Projects API
| Endpoint | Tipo | Descrição |
|----------|------|-----------|
| `projects.listProjects` | GET | Listar todos os projectos |
| `projects.listProjectsAndServices` | GET | Projectos + todos os serviços |
| `projects.createProject` | POST | Criar projecto (`name`) |
| `projects.destroyProject` | POST | Destruir projecto (`projectName`) |
| `projects.inspectProject` | GET | Inspecionar projecto (`projectName`) |
```bash
# Listar projectos (teste rápido de autenticação)
curl -s "http://localhost:3000/api/trpc/projects.listProjects" \
-H "Authorization: Bearer $TOKEN"
# Criar projecto
curl -s -X POST "http://localhost:3000/api/trpc/projects.createProject" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"json":{"name":"novo-projecto"}}'
```
---
## Services API
Endpoints completos com exemplos curl: ver [references/services-api.md](references/services-api.md)
**Verificados 12-03-2026** (engenharia reversa do backend.js minificado):
| Endpoint | Tipo | Descrição |
|----------|------|-----------|
| `services.app.createService` | POST | Criar serviço app |
| `services.postgres.createService` | POST | Criar PostgreSQL |
| `services.mysql.createService` | POST | Criar MySQL |
| `services.redis.createService` | POST | Criar Redis |
| `services.app.inspectService` | GET | Inspecionar serviço |
| `services.app.deployService` | POST | Fazer deploy |
| `services.app.stopService` | POST | Parar serviço |
| `services.app.startService` | POST | Iniciar serviço |
| `services.app.restartService` | POST | Reiniciar serviço |
| `services.app.destroyService` | POST | Destruir serviço |
**Endpoints que NAO existem** (removidos da versao anterior):
`enableService`, `disableService` — usar `startService`/`stopService` em vez disso.
**Parâmetros obrigatórios:** `projectName` + `serviceName` em todos os endpoints de serviços.
**Regra Descomplicar:** `projectName:"descomplicar"` para serviços próprios.
---
## Service Configuration API
Endpoints completos com exemplos curl: ver [references/service-config-api.md](references/service-config-api.md)
**Verificados 12-03-2026:**
| Endpoint | Descrição | Verificado |
|----------|-----------|:----------:|
| `services.app.updateSourceGithub` | Source GitHub (`owner`, `repo`, `ref`, `path`) | Sim |
| `services.app.updateSourceGit` | Source Git custom (`repo`, `ref`, `path`) — params no nivel raiz, NAO dentro de `source` | Sim |
| `services.app.updateSourceImage` | Source Docker image (`image`) | Sim |
| `services.app.updateEnv` | Variáveis de ambiente (`env` como string multi-linha) | Sim |
| `services.app.updateBuild` | Build config (`type`, `buildCommand`, `startCommand`) | Sim |
| `services.app.updateResources` | CPU/RAM (`memoryLimit`, `cpuLimit`, etc.) | Sim |
| `services.app.updateAdvanced` | Deploy avançado (`replicas`, `command`, `zeroDowntime`) | Sim |
| `services.app.updateRedirects` | Redireccionamentos HTTP | Sim |
| `services.app.updateBasicAuth` | Autenticacao basica | Sim |
| `services.app.updateMaintenance` | Modo manutencao | Sim |
**Endpoints que NAO existem na versao instalada:**
- ~~`services.app.updateDomains`~~ — dominios so via UI do EasyPanel
- ~~`services.app.updateMounts`~~ — nao encontrado
- ~~`services.app.updatePorts`~~ — nao encontrado
---
## Domains API (Verificado 12-03-2026)
Os dominios NAO sao geridos por `services.app.*` mas sim pelo namespace `domains.*`.
| Endpoint | Tipo | Descricao |
|----------|------|-----------|
| `domains.listDomains` | GET | Listar dominios do projecto (`projectName`) |
| `domains.createDomain` | POST | Criar dominio |
| `domains.updateDomain` | POST | Actualizar dominio |
| `domains.deleteDomain` | POST | Remover dominio |
```bash
# Listar dominios
INPUT='{"json":{"projectName":"descomplicar"}}'
ENCODED=$(python3 -c "import urllib.parse; print(urllib.par
@@ -0,0 +1,283 @@
# EasyPanel API - Service Configuration
**Verificado:** 12-03-2026 (engenharia reversa do backend.js)
---
## Actualizar Source (GitHub)
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateSourceGithub" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"owner":"ealmeida",
"repo":"MeuRepo",
"ref":"main",
"path":"/"
}}'
```
## Actualizar Source (Git Custom)
**IMPORTANTE:** Os parametros `repo`, `ref`, `path` devem estar no nivel RAIZ do JSON, NAO dentro de um objecto `source`.
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateSourceGit" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"repo":"https://ealmeida:TOKEN@git.descomplicar.pt/ealmeida/repo",
"ref":"main",
"path":"/"
}}'
```
**Errado (causa zodErrors):**
```json
{"json":{"projectName":"x","serviceName":"y","source":{"type":"git","repo":"...","ref":"main","path":"/"}}}
```
**Correcto:**
```json
{"json":{"projectName":"x","serviceName":"y","repo":"...","ref":"main","path":"/"}}
```
## Actualizar Source (Docker Image)
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateSourceImage" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"image":"node:22-alpine"
}}'
```
## Actualizar Environment Variables
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateEnv" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"env":"NODE_ENV=production\nPORT=3000\nDATABASE_URL=postgres://..."
}}'
```
## Actualizar Build Config
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateBuild" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"build":{
"type":"nixpacks",
"buildCommand":"npm run build",
"startCommand":"npm start"
}
}}'
```
## Actualizar Resources
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateResources" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"resources":{
"memoryLimit":"512m",
"memoryReservation":"256m",
"cpuLimit":1,
"cpuReservation":0.5
}
}}'
```
## Actualizar Deploy (Replicas, Command)
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateAdvanced" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"deploy":{
"replicas":2,
"command":"node dist/index.js",
"zeroDowntime":true
}
}}'
```
## Actualizar Redirects
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateRedirects" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"redirects":[]
}}'
```
## Actualizar Basic Auth
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateBasicAuth" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"basicAuth":{"enabled":false}
}}'
```
## Actualizar Maintenance Mode
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.updateMaintenance" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-api",
"maintenance":{"enabled":false}
}}'
```
## Database Backup Config
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.postgres.updateBackup" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"projectName":"descomplicar",
"serviceName":"minha-db",
"backup":{
"enabled":true,
"schedule":"0 3 * * *",
"destination":{
"type":"s3",
"bucket":"backups",
"region":"eu-west-1",
"accessKey":"...",
"secretKey":"..."
}
}
}}'
```
---
## Domains API (Namespace separado)
**IMPORTANTE:** Os dominios NAO sao geridos por `services.app.*` mas sim pelo namespace `domains.*`.
### Listar Dominios (GET)
```bash
INPUT='{"json":{"projectName":"descomplicar"}}'
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$INPUT'))")
curl -s "http://localhost:3000/api/trpc/domains.listDomains?input=$ENCODED" \
-H "Authorization: Bearer $TOKEN"
```
### Criar Dominio (POST)
```bash
curl -s -X POST "http://localhost:3000/api/trpc/domains.createDomain" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{
"id":"meu-dominio-id",
"https":true,
"host":"app.descomplicar.pt",
"path":"/",
"middlewares":[],
"certificateResolver":"letsencrypt",
"wildcard":false,
"destinationType":"service",
"serviceDestination":{
"protocol":"http",
"port":3000,
"path":"/",
"projectName":"descomplicar",
"serviceName":"meu-servico"
}
}}'
```
**Notas:**
- O `id` pode ser qualquer string unica (cuid ou slug)
- `certificateResolver`: `"letsencrypt"` para HTTPS, `""` para dominios internos
- `destinationType`: `"service"` para servicos EasyPanel, `"custom"` para destinos externos
### Actualizar Dominio (POST)
```bash
curl -s -X POST "http://localhost:3000/api/trpc/domains.updateDomain" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{...mesmo schema de createDomain...}}'
```
### Remover Dominio (POST)
```bash
curl -s -X POST "http://localhost:3000/api/trpc/domains.deleteDomain" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"id":"meu-dominio-id"}}'
```
---
## Resumo de Endpoints de Configuracao
| Endpoint | Tipo | Descricao | Verificado |
|----------|------|-----------|:----------:|
| `services.app.updateSourceGithub` | POST | Source GitHub (`owner`,`repo`,`ref`,`path`) | Sim |
| `services.app.updateSourceGit` | POST | Source Git custom (`repo`,`ref`,`path` RAIZ) | Sim |
| `services.app.updateSourceImage` | POST | Source Docker image (`image`) | Sim |
| `services.app.updateEnv` | POST | Variaveis de ambiente (`env` string) | Sim |
| `services.app.updateBuild` | POST | Build config (`type`,`buildCommand`,`startCommand`) | Sim |
| `services.app.updateResources` | POST | CPU/RAM limits | Sim |
| `services.app.updateAdvanced` | POST | Deploy avancado (replicas, command, zeroDowntime) | Sim |
| `services.app.updateRedirects` | POST | Redireccionamentos HTTP | Sim |
| `services.app.updateBasicAuth` | POST | Autenticacao basica | Sim |
| `services.app.updateMaintenance` | POST | Modo manutencao | Sim |
| `domains.listDomains` | GET | Listar dominios (`projectName`) | Sim |
| `domains.createDomain` | POST | Criar dominio | Sim |
| `domains.updateDomain` | POST | Actualizar dominio | Sim |
| `domains.deleteDomain` | POST | Remover dominio | Sim |
**Endpoints que NAO existem na versao instalada:**
- ~~`services.app.updateDomains`~~ — usar `domains.createDomain`
- ~~`services.app.updateMounts`~~ — nao encontrado
- ~~`services.app.updatePorts`~~ — nao encontrado
- ~~`services.app.saveDomains`~~ — nao existe
- ~~`services.app.saveGithubSource`~~ — usar `updateSourceGithub` ou `updateSourceGit`
---
*Actualizado: 12-03-2026*
@@ -0,0 +1,135 @@
# EasyPanel API - Services
**Verificado:** 12-03-2026 (engenharia reversa do backend.js)
## Service Types
| Type | Descricao |
|------|-----------|
| `app` | Aplicacao (Node.js, Python, Go, Rust, etc.) |
| `mysql` | MySQL database |
| `mariadb` | MariaDB database |
| `postgres` | PostgreSQL database |
| `mongo` | MongoDB database |
| `redis` | Redis cache |
## Criar Servico App
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.createService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"minha-api"}}'
```
## Criar Servico Database
```bash
# PostgreSQL
curl -s -X POST "http://localhost:3000/api/trpc/services.postgres.createService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"minha-db"}}'
# MySQL
curl -s -X POST "http://localhost:3000/api/trpc/services.mysql.createService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"mysql-db"}}'
# Redis
curl -s -X POST "http://localhost:3000/api/trpc/services.redis.createService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"cache"}}'
```
## Inspeccionar Servico (GET)
```bash
INPUT='{"json":{"projectName":"descomplicar","serviceName":"dashboard_descomplicar"}}'
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$INPUT'))")
curl -s "http://localhost:3000/api/trpc/services.app.inspectService?input=$ENCODED" \
-H "Authorization: Bearer $TOKEN"
```
Response parcial:
```json
{
"projectName": "descomplicar",
"name": "dashboard_descomplicar",
"type": "app",
"enabled": true,
"token": "deploy-webhook-token",
"source": {"type": "git", "repo": "https://...", "ref": "main"},
"build": {"type": "nixpacks", "buildCommand": "npm run build"},
"env": "VAR=value",
"mounts": [],
"ports": []
}
```
## Deploy Servico
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.deployService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"minha-api"}}'
```
## Stop / Start / Restart Servico
```bash
# Parar servico
curl -s -X POST "http://localhost:3000/api/trpc/services.app.stopService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"minha-api"}}'
# Iniciar servico
curl -s -X POST "http://localhost:3000/api/trpc/services.app.startService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"minha-api"}}'
# Reiniciar servico
curl -s -X POST "http://localhost:3000/api/trpc/services.app.restartService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"minha-api"}}'
```
**IMPORTANTE:** `enableService` e `disableService` NAO existem na versao instalada. Usar `startService`/`stopService`.
## Destruir Servico
```bash
curl -s -X POST "http://localhost:3000/api/trpc/services.app.destroyService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"descomplicar","serviceName":"minha-api"}}'
```
---
## Resumo de Endpoints
| Endpoint | Tipo | Descricao | Verificado |
|----------|------|-----------|:----------:|
| `services.app.createService` | POST | Criar servico app | Sim |
| `services.postgres.createService` | POST | Criar PostgreSQL | Sim |
| `services.mysql.createService` | POST | Criar MySQL | Sim |
| `services.redis.createService` | POST | Criar Redis | Sim |
| `services.app.inspectService` | GET | Inspeccionar servico | Sim |
| `services.app.deployService` | POST | Fazer deploy | Sim |
| `services.app.stopService` | POST | Parar servico | Sim |
| `services.app.startService` | POST | Iniciar servico | Sim |
| `services.app.restartService` | POST | Reiniciar servico | Sim |
| `services.app.destroyService` | POST | Destruir servico | Sim |
**Endpoints que NAO existem:** `enableService`, `disableService`, `redeployService`
---
*Actualizado: 12-03-2026*
@@ -0,0 +1,184 @@
---
name: easypanel-cicd
description: Configuracao de CI/CD com Gitea Actions para auto-deploy no EasyPanel via webhook — setup de workflows, secrets e validacao.
layer: wiki
---
# /easypanel-cicd - CI/CD Gitea Actions para EasyPanel
Configuracao completa de pipelines CI/CD que ligam repositorios Gitea ao EasyPanel para auto-deploy.
---
## Quando usar
- Configurar auto-deploy de um repositorio Gitea para EasyPanel
- Criar workflow Gitea Actions com testes + deploy
- Obter e configurar webhook URL de um servico
- Diagnosticar pipelines CI/CD falhados
- Adicionar CI/CD a projectos existentes no EasyPanel
## Quando nao usar
- Para deploy manual (usar `/easypanel-deploy`)
- Para scaffold de projecto novo (usar `/easypanel-init`)
- Para troubleshooting de containers (usar `/easypanel-troubleshoot`)
---
## Sintaxe
```bash
/easypanel-cicd <repo-name> # Setup completo (workflow + secrets + webhook)
/easypanel-cicd <repo-name> --webhook-only # Apenas obter/configurar webhook
/easypanel-cicd <repo-name> --diagnose # Diagnosticar pipeline falhado
```
---
## Arquitectura
```
Gitea (push main) --> Gitea Actions Runner --> Testes + Build --> Webhook POST --> EasyPanel Redeploy
(3 runners activos) (deploy URL) (build + container)
```
**Componentes:**
- **Gitea Actions Runners:** 3 replicas activas (`descomplicar_gitea-runner`)
- **Webhook URL:** Cada servico EasyPanel tem `deploymentUrl` unico
- **Formato:** `http://<IP>:3000/api/deploy/<service-token>`
---
## Workflow completo
### 1. Obter webhook URL do servico
```bash
TOKEN=$(cat /etc/easypanel/.api-token)
# Via inspectService — campo deploymentUrl
INPUT='{"json":{"projectName":"descomplicar","serviceName":"SERVICE_NAME"}}'
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$INPUT'))")
curl -s "http://localhost:3000/api/trpc/services.app.inspectService?input=$ENCODED" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result']['data']['json']['deploymentUrl'])"
```
**Formato da URL:** `http://178.63.18.51:3000/api/deploy/<service-token>`
**Nota:** O token e unico por servico e nao expira. Nao confundir com o API token geral.
### 2. Configurar secret no repositorio Gitea
Via MCP Gitea:
```
mcp__gitea__upsert_repo_action_secret({
owner: "ealmeida",
repo: "REPO_NAME",
secretname: "EASYPANEL_DEPLOY_URL",
body: { data: "http://178.63.18.51:3000/api/deploy/<service-token>" }
})
```
Secrets adicionais recomendados:
- `EASYPANEL_DEPLOY_URL` — webhook URL (obrigatorio)
- `DEPLOY_DOMAIN` — dominio do servico (opcional, para health check)
### 3. Criar workflow Gitea Actions
Criar `.gitea/workflows/deploy.yml` no repositorio:
#### Template basico (apenas deploy)
```yaml
name: Deploy to EasyPanel
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger EasyPanel Deploy
run: |
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${{ secrets.EASYPANEL_DEPLOY_URL }}")
if [ "$RESPONSE" != "200" ]; then
echo "Deploy trigger failed with HTTP $RESPONSE"
exit 1
fi
echo "Deploy triggered successfully"
```
#### Template completo (testes + build + deploy + health check)
```yaml
name: CI/CD Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint --if-present
- name: Test
run: npm test --if-present
- name: Build
run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Trigger EasyPanel Deploy
run: |
echo "Triggering deploy..."
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${{ secrets.EASYPANEL_DEPLOY_URL }}")
if [ "$RESPONSE" != "200" ]; then
echo "Deploy trigger failed with HTTP $RESPONSE"
exit 1
fi
echo "Deploy triggered (HTTP $RESPONSE)"
- name: Wait for deployment
run: sleep 120
- name: Health check
if: ${{ secrets.DEPLOY_DOMAIN != '' }}
run: |
for i in 1 2 3 4 5; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ secrets.DEPLOY_DOMAIN }}/health" || echo "000")
echo "Health check $i/5: HTTP $STATUS"
if [ "$STATUS" = "200" ]; then
echo "Service healthy"
exit 0
fi
sleep 15
done
echo "Health check failed after 5 attempts"
e
@@ -0,0 +1,233 @@
---
name: easypanel-deploy
description: Deploy completo e automatizado para EasyPanel com validação integrada, monitoring e safety net (auto-rollback) via API oficial.
disable-model-invocation: true
layer: wiki
---
# EasyPanel Deploy
Deploy completo e automatizado para EasyPanel com validação integrada.
## Quando Usar
- Deploy completo de projecto
- Deploy com validação automática
- Deploy com monitoring
- Deploy com safety net (auto-rollback)
- Production deploys críticos
## Sintaxe
```bash
/easypanel-deploy [--skip-tests] [--force]
```
## Exemplos
```bash
# Deploy normal (com tests e validation)
/easypanel-deploy
# Deploy sem tests (CI/CD já rodou)
/easypanel-deploy --skip-tests
# Force deploy (ignorar warnings)
/easypanel-deploy --force
```
## Workflow Completo (9 Steps)
### 1. Pre-deploy Validation
```bash
/easypanel-validate
```
**Gates:**
- Score < 7/10 → **ABORT** (excepto --force)
- Critical issues → **ABORT**
- Se pass → Continue
### 2. Run Tests (se não --skip-tests)
```bash
npm test
```
**Gates:**
- Tests fail → **ABORT**
- No tests found → **WARNING** (continue)
- Tests pass → Continue
### 3. Build Local (test)
```bash
docker build --no-cache -t <project>:test .
```
**Checks:**
- Build completa sem erros
- Image size < 200MB (Node.js)
- EXPOSE port correcto
**Gates:**
- Build fail → **ABORT**
- Image size > 500MB → **WARNING** (continue com --force)
### 4. Git Check
```bash
git status
```
**Checks:**
- Uncommitted changes?
- Untracked files?
**Actions:**
- Se changes → **PROMPT**: "Commit agora? [y/N]"
- Se yes → `git add . && git commit -m "deploy: ..."`
- Se no → **ABORT**
### 5. Push to Gitea
```bash
git push origin main
```
**Actions:**
- Push commits
- Se CI/CD configurado → Aguardar webhook trigger
- Log commit SHA
### 6. Monitor EasyPanel Deploy
**Via API (preferido - sincroniza com GUI):**
```bash
# Obter token
TOKEN=$(cat /etc/easypanel/.api-token)
# Verificar estado do serviço
curl -s "http://localhost:3000/api/trpc/services.app.inspectService?input=$(echo -n '{"json":{"projectName":"PROJECT","serviceName":"SERVICE"}}' | jq -sRr @uri)" \
-H "Authorization: Bearer $TOKEN"
# Listar todos os serviços do projecto
curl -s "http://localhost:3000/api/trpc/projects.inspectProject?input=$(echo -n '{"json":{"projectName":"PROJECT"}}' | jq -sRr @uri)" \
-H "Authorization: Bearer $TOKEN"
```
**Via SSH (fallback):**
```bash
# Poll cada 10s (timeout 5min)
docker service ls | grep <service>
docker service ps <service>
```
**Timeline:**
- [00:00] Build started
- [01:30] Build complete
- [02:00] Container starting
- [02:15] Health check iniciado
### 7. Health Check (retry 5x, interval 10s)
```bash
curl -f https://<domain>/health
```
**Retry Logic:**
- Tentativa 1-5: aguardar 10s entre cada
- Se 200 OK → **SUCCESS**
- Se 5xx após 5 retries → **ROLLBACK**
- Se timeout → **ROLLBACK**
### 8. Metrics Validation
```bash
# Via docker stats ou EasyPanel API
docker stats <container> --no-stream
```
**Checks:**
- CPU < 80% (avg 60s)
- Memory < 80% limit
- No errors em logs (últimos 60s)
**Gates:**
- CPU > 90% → **WARNING** (potential issue)
- Memory > 90% → **WARNING** (OOM risk)
- Errors em logs → **WARNING** + report
### 9. Success Report
**Output:**
- Total deploy time
- Build time
- Image size
- Health status
- Metrics snapshot
- URLs (production, health, logs)
## Output Format (Success)
```
🚀 EasyPanel Deploy: <project-name>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏱️ TIMELINE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[00:00] ✅ Pre-deploy validation (score: 9.2/10)
[00:15] ✅ Tests passed (12/12, 2.3s)
[00:20] ✅ Local build (82MB, 1m 34s)
[00:25] ✅ Git push (commit: a3f5c21)
[01:55] ✅ EasyPanel build started
[03:42] ✅ Container started (dashboard-api-6f9d8)
[03:50] ✅ Health check 1/5: 200 OK
[03:52] ✅ Health check 5/5: 200 OK (stable)
[04:00] ✅ Metrics validated (CPU: 12%, RAM: 45MB/512MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ DEPLOY SUCCESS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Metrics:
- Total time: 4m 0s (vs ~60min manual, -93%)
- Build time: 1m 34s
- Image size: 82MB (-80% vs non-multi-stage)
- Cold start: 2.1s
- Memory usage: 45MB / 512MB (8.8%)
- CPU usage: 12% (avg first 60s)
🌐 Endpoints:
- Production: https://domain.descomplicar.pt
- Health: https://domain.descomplicar.pt/health
📋 Logs:
- EasyPanel: https://easy.descomplicar.pt/services/<service>/logs
- Gitea: https://git.descomplicar.pt/org/repo/actions
🔔 Next Steps:
1. Monitor uptime (first 24h critical)
2. Check error rate (should be <1%)
3. Load testing (optional, recommended)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## Output Format (Failed)
```
❌ DEPLOY FAILED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏱️ TIMELINE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[00:00] ✅ Pre-deploy validation
[00:15] ✅ Tests passed
[00:20] ✅ Local build
[00:25] ✅ Git push
[01:55] ✅ EasyPanel build started
[03:42]
@@ -0,0 +1,248 @@
---
name: easypanel-init
description: Inicialização de projectos EasyPanel com configuração adequada via API oficial — scaffold automático para Node.js, PHP ou sites estáticos.
disable-model-invocation: true
layer: wiki
---
# EasyPanel Init
Scaffold automático de projectos optimizados para deploy no EasyPanel.
## Quando Usar
- Iniciar novo projecto do zero
- Converter projecto existente para EasyPanel
- Setup rápido com best practices
- Criar estrutura standardizada
- Prototype rapidamente
## Sintaxe
```bash
/easypanel-init <project-name> <domain> [--type nodejs|python|go] [--db postgres|mysql|none]
```
## Exemplos
```bash
# Node.js API sem database
/easypanel-init dashboard-api dashboard.descomplicar.pt --type nodejs
# Node.js API com PostgreSQL
/easypanel-init crm-api crm.descomplicar.pt --type nodejs --db postgres
# Python API (futuro)
/easypanel-init ml-api ml.descomplicar.pt --type python
# Go API (futuro)
/easypanel-init fast-api api.descomplicar.pt --type go
```
## Workflow Completo
### 1. Criar Estrutura de Pastas
```
project-root/
├── src/
│ ├── index.ts (ou main.py, main.go)
│ └── routes/
│ └── health.ts
├── Dockerfile (template multi-stage)
├── docker-compose.yml (Traefik labels)
├── .dockerignore
├── .gitignore
├── .env.example
├── package.json (scripts padronizados)
├── tsconfig.json (se TypeScript)
├── .gitea/
│ └── workflows/
│ └── deploy.yml (CI/CD)
└── README.md (deploy guide)
```
### 2. Generate Templates
#### Dockerfile (Multi-stage Node.js)
```dockerfile
# Stage 1: Dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Builder
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Runner
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
CMD ["npm", "start"]
```
#### docker-compose.yml (Traefik)
```yaml
version: '3.8'
services:
app:
build: .
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.PROJECTNAME.rule=Host(`DOMAIN`)"
- "traefik.http.routers.PROJECTNAME.entrypoints=websecure"
- "traefik.http.routers.PROJECTNAME.tls.certresolver=letsencrypt"
- "traefik.http.services.PROJECTNAME.loadbalancer.server.port=3000"
- "traefik.http.services.PROJECTNAME.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.PROJECTNAME.loadbalancer.healthcheck.interval=10s"
networks:
traefik:
external: true
```
#### Health Endpoint (Express)
```typescript
import { Router } from 'express'
import type { Request, Response } from 'express'
const router = Router()
router.get('/health', (req: Request, res: Response) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development'
})
})
export default router
```
#### Gitea Workflow (.gitea/workflows/deploy.yml)
```yaml
name: Deploy to EasyPanel
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Trigger EasyPanel Deploy
run: |
curl -X POST ${{ secrets.EASYPANEL_WEBHOOK_URL }}
```
#### package.json (scripts)
```json
{
"name": "PROJECT_NAME",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint .",
"test": "vitest"
}
}
```
#### README.md
```markdown
# PROJECT_NAME
## Setup Local
1. npm install
2. cp .env.example .env.local
3. npm run dev
## Deploy EasyPanel
1. Push to main: `git push origin main`
2. CI/CD auto-deploy via Gitea Actions
3. Health check: https://DOMAIN/health
## Environment Variables
See `.env.example` for required variables.
## Architecture
- Multi-stage Docker build (80% smaller image)
- Traefik routing with SSL
- Health checks every 10s
- Non-root user for security
```
### 3. Initialize Git
```bash
git init
git add .
git commit -m "chore: initial project setup via /easypanel-init"
```
### 4. Criar Projecto no EasyPanel via API
```bash
# Obte
@@ -0,0 +1,204 @@
---
name: easypanel-monitor
description: Monitorizacao de servicos EasyPanel -- estado de todos os servicos Docker, uso de recursos, disco e logs de erros com tabela resumo OK/DOWN/WARN.
layer: wiki
---
# /easypanel-monitor - Monitorizacao EasyPanel
Verificacao rapida do estado de todos os servicos no EasyPanel (VM 101) com identificacao de problemas.
---
## Quando usar
- Verificacao diaria do estado dos servicos
- Apos alertas de servicos down
- Antes de deploys (validar estado actual)
- Diagnostico rapido de problemas de recursos
## Quando nao usar
- Para troubleshooting aprofundado de um servico especifico (usar `/easypanel-troubleshoot`)
- Para rollback de versao (usar `/easypanel-rollback`)
- Para deploy de servicos (usar `/easypanel-deploy`)
---
## Sintaxe
```bash
/easypanel-monitor # Check completo (servicos + recursos + disco)
/easypanel-monitor quick # Apenas estado dos servicos (OK/DOWN)
/easypanel-monitor <servico> # Detalhe de um servico especifico (logs + stats)
```
---
## Servidor alvo
| Alias SSH | IP | Porta | Funcao |
|-----------|-----|-------|--------|
| **easy** | 5.9.90.70 | 22 | EasyPanel (VM 101) |
```javascript
mcp__ssh-unified__ssh_execute({
server: "easy",
command: "<comando>"
})
```
---
## Workflow completo
### Passo 1: Estado dos servicos (docker service ls)
```bash
docker service ls --format "table {{.Name}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}"
```
**Classificacao:**
- **OK** - Replicas X/X (todas activas)
- **DOWN** - Replicas 0/X (nenhuma activa)
- **WARN** - Replicas parciais ou servico sem replicas esperadas
### Passo 2: Identificar servicos DOWN (0/N replicas)
```bash
docker service ls --format "{{.Name}} {{.Replicas}}" | awk '$2 ~ /^0\//'
```
Para cada servico DOWN, obter logs:
```bash
docker service logs <nome-servico> --tail 10 --no-trunc 2>&1
```
### Passo 3: Uso de recursos via API (preferido)
```bash
TOKEN=$(cat /etc/easypanel/.api-token)
# Stats do sistema — retorna estrutura aninhada
curl -s "http://localhost:3000/api/trpc/monitor.getSystemStats" \
-H "Authorization: Bearer $TOKEN"
```
**Estrutura real da resposta (verificado 24-03-2026):**
```json
{
"result": {
"data": {
"json": {
"uptime": 3118351.41,
"memInfo": {
"totalMemMb": 32096.51,
"usedMemMb": 16885.52,
"freeMemMb": 15210.99,
"usedMemPercentage": 52.61,
"freeMemPercentage": 47.39
},
"diskInfo": {
"totalGb": "192.7",
"usedGb": "89.7",
"freeGb": "103.0",
"usedPercentage": "46.5",
"freePercentage": "53.5"
},
"cpuInfo": {
"usedPercentage": 13.65,
"count": 6,
"loadavg": [5.58, 6.34, 6.51]
},
"network": {
"inputMb": 0,
"outputMb": 0
}
}
}
}
}
```
**Campos a usar:**
- CPU: `cpuInfo.usedPercentage`
- RAM: `memInfo.usedMemPercentage` e `memInfo.usedMemMb`
- Disco: `diskInfo.usedPercentage` e `diskInfo.usedGb`
**Fallback via SSH (se API indisponivel):**
```bash
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}"
```
**Thresholds:**
| Metrica | OK | WARN | CRITICAL |
|---------|-----|------|----------|
| CPU | <70% | 70-90% | >90% |
| RAM | <70% | 70-85% | >85% |
| RAM absoluto | <512MB | 512MB-1GB | >1GB |
### Passo 4: Espaco em disco (via API ou fallback)
Usar `diskInfo` da resposta do passo 3. Fallback:
```bash
df -h / /var/lib/docker
```
**Thresholds:**
| Disco | OK | WARN | CRITICAL |
|-------|-----|------|----------|
| / | <70% | 70-85% | >85% |
| /var/lib/docker | <70% | 70-85% | >85% |
Verificar imagens Docker nao utilizadas:
```bash
docker system df
```
### Passo 5: Logs de erros recentes (servicos com problemas)
Para servicos DOWN ou WARN:
```bash
docker service logs <nome-servico> --tail 10 --no-trunc 2>&1 | grep -iE "error|fatal|panic|oom|killed|fail"
```
### Passo 6: Verificar tarefas falhadas
```bash
docker service ls -q | xargs -I {} docker service ps {} --format "{{.Name}} {{.CurrentState}} {{.Error}}" --filter "desired-state=shutdown" 2>/dev/null | grep -i "failed\|rejected\|error" | head -20
```
---
## Template de output
```markdown
## EasyPanel Monitor - [DATA]
**Servidor:** easy.descomplicar.pt (5.9.90.70)
### Estado dos servicos
| Servico | Replicas | Estado | CPU | RAM | Notas |
|---------|----------|--------|-----|-----|-------|
| descomplicar_api | 1/1 | OK | 2.3% | 128MB | - |
| descomplicar_n8n | 1/1 | OK | 5.1% | 256MB | - |
| descomplicar_db | 0/1 | DOWN | - | - | Connection refused |
| descomplicar_redis | 1/1 | WARN | 85% | 490MB | CPU elevado |
**Resumo:** X/Y servicos OK | Z DOWN | W WARN
### Disco
| Mount | Usado | Disponivel | % | Estado |
|-------|-------|------------|---|--------|
| / | 45G | 55G | 45% | OK |
| /var/lib/docker | 30G | 70G | 30% | OK |
Docker system: X imagens, Y containers, Z volumes (
@@ -0,0 +1,249 @@
---
name: easypanel-rollback
description: Rollback seguro de deployments EasyPanel para versões anteriores estáveis via API oficial.
disable-model-invocation: true
layer: wiki
---
# EasyPanel Rollback
Rollback rápido e seguro para versões anteriores estáveis.
## Quando Usar
- Deploy falhou (health checks fail)
- Aplicação com erros em produção
- Rollback urgente necessário
- Testar versão anterior
- Recovery após incidente
## Sintaxe
```bash
/easypanel-rollback <service-name> [--to-version <version>] [--auto]
```
## Exemplos
```bash
# Rollback para versão anterior imediata
/easypanel-rollback dashboard-api
# Rollback para tag específica
/easypanel-rollback dashboard-api --to-version v1.2.5
# Rollback automático (sem confirmação)
/easypanel-rollback dashboard-api --auto
```
## Workflow Completo
### 1. List Recent Versions
```bash
git log --oneline -n 10
git tag --sort=-creatordate | head -n 10
```
**Output:**
```
v1.3.0 (current) ❌ FAILING - Health: 502
v1.2.5 (previous) ✅ STABLE - Health: 200 OK, Uptime: 99.8%
v1.2.4 ✅ STABLE
v1.2.3 ✅ STABLE
```
**Metadata tracking:**
- Commit SHA
- Deploy timestamp
- Health status (last known)
- Uptime percentage (if tracked)
- Issue description (se falhou)
### 2. Select Target Version
**Strategies:**
#### A. Interactive (default)
```
➜ Rollback to: [2] v1.2.5 (recommended)
Available versions:
1. v1.3.0 (current) ❌ FAILING
2. v1.2.5 (previous) ✅ STABLE (recommended)
3. v1.2.4 ✅ STABLE
4. v1.2.3 ✅ STABLE
Enter number [2]:
```
#### B. Automatic (--auto)
```
Last known good: v1.2.5 (health OK, deployed 2h ago)
Auto-selecting v1.2.5...
```
#### C. Specific (--to-version)
```
Target version specified: v1.2.5
Validating...
```
### 3. Confirm Rollback (se não --auto)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 ROLLBACK PLAN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Current: v1.3.0 (a3f5c21) - Port mismatch issue
Target: v1.2.5 (7d3a1f2) - Stable, 99.8% uptime
Changes (diff):
- src/server.ts: PORT 8080 → 3000
- docker-compose.yml: Traefik port fix
+ Reverts 3 commits
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Confirm rollback? [Y/n]:
```
### 4. Perform Rollback
**Method A: Git Revert (small rollback, 1-3 commits)**
```bash
git revert --no-commit a3f5c21..HEAD
git commit -m "revert: rollback to v1.2.5 due to port mismatch"
```
**Method B: Git Checkout (large rollback, >3 commits)**
```bash
git checkout 7d3a1f2
git checkout -b rollback-to-v1.2.5
git push origin rollback-to-v1.2.5:main --force
```
### 5. Push & Redeploy
```bash
git push origin main
```
**Via API (preferido - sincroniza com GUI):**
```bash
# Obter token
TOKEN=$(cat /etc/easypanel/.api-token)
# Trigger deploy via API (redeployService NAO existe, usar deployService)
curl -s -X POST "http://localhost:3000/api/trpc/services.app.deployService" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"json":{"projectName":"PROJECT","serviceName":"SERVICE"}}'
```
**Se CI/CD configurado:**
- Trigger automático via webhook
- Aguardar build (timeout 5min)
### 6. Monitor Rollback
```bash
# Poll EasyPanel deploy
docker service ps <service>
docker service logs <service> --tail 50 --follow
```
**Timeline:**
```
[00:00] ✅ Git revert completed
[00:05] ✅ Push to Gitea
[01:45] ✅ EasyPanel build started
[03:20] ✅ Container started
[03:28] ✅ Health check: 200 OK
```
### 7. Validate Rollback
#### Health Check (5 retries)
```bash
curl -f https://domain/health
```
**Expected:** 200 OK
#### Logs Check
```bash
docker logs <container> --tail 100 | grep -i error
```
**Expected:** No critical errors
#### Metrics Check
```bash
docker stats <container> --no-stream
```
**Expected:**
- CPU < 80%
- RAM < 80%
- No OOM kills
### 8. Success Report
```
✅ ROLLBACK COMPLETE
Total time: 3m 30s
Service restored: https://domain.descomplicar.pt
Health: ✅ 200 OK
Version: v1.2.5 (7d3a1f2)
🛠️ NEXT STEPS
1. Investigate root cause: /easypanel-troubleshoot
2. Fix issues locally
3. Test fix: docker build + docker run
4. Redeploy: /easypanel-deploy
```
## Output Format (Success)
```
🔄 EasyPanel Rollback: dashboard-api
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📜 AVAILABLE VERSIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. v1.3.0 (current) ❌ FAILING
- Commit: a3f5c21
- Deployed: 15min ago
- Health: 502 Bad Gateway
- Issue: Port mismatch
2. v1.2.5 (previous) ✅ STABLE
- Commit: 7d3a1f2
- Deployed: 2h ago
- Health: 200 OK (before upgrade)
- Uptime: 99.8% (last 7 days)
3. v1.2.4 ✅ STABLE
- Commit: 9c4b3a1
- Deployed: 1 day ago
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
➜ Rollback to: v1.2.5 (recommended)
Confirm? [Y/n]: y
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏱️ ROLLBACK IN PROGRESS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[00:00] ✅ Git revert (a3f5c21 → 7d3a1f2)
[00:05] ✅ Commit rollback
[00:10] ✅ Push to Gitea
[01:45] ✅ EasyPanel redeploy started
[03:20] ✅ Container started
[03:28] ✅ Health
@@ -0,0 +1,205 @@
---
name: easypanel-troubleshoot
description: Diagnóstico automático de problemas de deploy no EasyPanel — análise de logs de containers, health endpoints, routing Traefik e recursos para identificar causas raiz.
layer: wiki
---
# EasyPanel Troubleshoot
Diagnóstico automático e inteligente de problemas de deploy no EasyPanel.
## Quando Usar
- Deploy falhou com erros 502/503
- Container em crash loop (restarts frequentes)
- Health endpoint não responde
- Problemas de routing Traefik
- Investigar causa de deploy falhado
- After deploy validation
## Quando NÃO Usar
- Para deploys iniciais (usar `/easypanel-init`)
- Para rollbacks de versão (usar `/easypanel-rollback`)
- Para validação pós-deploy bem-sucedido (usar `/easypanel-validate`)
- Quando o problema é conhecido e a solução já está identificada
## Sintaxe
```bash
/easypanel-troubleshoot <service-name> [--verbose]
```
## Exemplos
```bash
# Diagnóstico básico
/easypanel-troubleshoot dashboard-api
# Diagnóstico detalhado (logs completos)
/easypanel-troubleshoot dashboard-api --verbose
```
## Workflow Automático
### 1. Check Service Status via API
```bash
# Obter token
TOKEN=$(cat /etc/easypanel/.api-token)
# Inspeccionar serviço (estado completo)
curl -s "http://localhost:3000/api/trpc/services.app.inspectService?input=$(echo -n '{"json":{"projectName":"PROJECT","serviceName":"SERVICE"}}' | jq -sRr @uri)" \
-H "Authorization: Bearer $TOKEN" | jq '.result.data.json'
# Obter logs via API (namespace logs.*, NAO services.app.*)
curl -s "http://localhost:3000/api/trpc/logs.getServiceLogs?input=$(echo -n '{"json":{"projectName":"PROJECT","serviceName":"SERVICE","tail":100}}' | jq -sRr @uri)" \
-H "Authorization: Bearer $TOKEN"
```
### 2. Fetch Logs (alternativa SSH)
```bash
docker logs <container> --tail 100
```
Parse de erros comuns:
- Port already in use (EADDRINUSE)
- Cannot find module (dependencies)
- Connection refused (database)
- Out of memory (OOM)
- Unhandled promise rejection
- Listen errors
### 3. Check Health Endpoint
```bash
curl https://<domain>/health
```
- Status code: 200, 404, 502, 503?
- Response time
- Response body validation
### 4. Check Traefik Routing
- Traefik labels correctos?
- router.rule com domain
- service.loadbalancer.server.port
- entrypoints=websecure
- certresolver=letsencrypt
- Domain DNS aponta para servidor?
- SSL certificate válido?
### 5. Check Port Mismatch
- Dockerfile EXPOSE vs app actual port
- docker-compose ports vs Traefik port
- App listening port (from logs)
- Common mismatch: 3000 vs 8080
### 6. Check Environment Variables
- Required vars set? (compare .env.example)
- Sensitive vars (DATABASE_URL, API_KEY)
- Missing vars causing app crash?
### 7. Check Dependencies
- Database reachable? (if applicable)
- Redis reachable? (if applicable)
- External APIs responding?
### 8. Check Resources
- CPU throttling?
- Memory limit reached (OOM)?
- Disk space?
### 9. Generate Report
- Issues found (prioritized: CRITICAL, WARNING, INFO)
- Root cause (if detected)
- Recommended fixes (step-by-step)
- Commands to run
## Error Pattern Detection
### Port Mismatch
```
Pattern: "listening on port X" + Traefik port Y
Fix: Update Dockerfile EXPOSE or Traefik label
```
### Missing Environment Variable
```
Pattern: "is not defined" / "undefined"
Fix: Add variable to EasyPanel environment
```
### Crash Loop
```
Pattern: Restart count > 3 in 10min
Fix: Check logs for root cause
```
### Database Connection
```
Pattern: "ECONNREFUSED" / "Connection refused"
Fix: Verify DATABASE_URL and network connectivity
```
### Memory Issues
```
Pattern: "out of memory" / "OOM killed"
Fix: Increase memory limit in docker-compose
```
## Output Format
```
🔍 EasyPanel Troubleshooting: <service-name>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 SERVICE STATUS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Container: <name>
Status: Running | Stopped | Restarting
Uptime: <time>
Restarts: <count> (last 10min)
Memory: X / Y MB
CPU: X%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 ISSUES FOUND (X)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 🔴 CRITICAL: <Issue Title>
Detected: <details>
Impact: <what breaks>
Fix:
<step-by-step solution>
2. ⚠️ WARNING: <Issue Title>
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 LOGS (Last 20 lines)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[timestamp] INFO: ...
[timestamp] ❌ ERROR: ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🛠️ RECOMMENDED ACTIONS (Prioritized)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. <Action 1>
2. <Action 2>
3. Redeploy: git commit + push
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔗 HELPFUL RESOURCES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Research: /media/ealmeida/Dados/Dev/Docs/EasyPanel-Deploy-Research/
- Checklist: CHECKLIST_EasyPanel_Deploy.md
- Templates: TEMPLATE_Dockerfile_NodeJS_MultiStage
```
## API Endpoints Usados
Ver skill `/easypanel-api` para documentação completa
@@ -0,0 +1,223 @@
---
name: easypanel-validate
description: Validação pré-deploy de projectos EasyPanel com health checks, verificação de serviços e auto-fix opcional via API oficial.
layer: wiki
---
# EasyPanel Validate
Validação pré-deploy de projectos EasyPanel com auto-fix opcional.
## Quando Usar
- Antes de deploy (pre-flight check)
- Após criar novo projecto
- Para auditar projecto existente
- Quality gate antes de commit
- CI/CD validation step
## Sintaxe
```bash
/easypanel-validate [--fix] [--strict]
```
## Exemplos
```bash
# Validação sem correcções
/easypanel-validate
# Validação com auto-fix (cria ficheiros em falta)
/easypanel-validate --fix
# Modo strict (fail on warnings)
/easypanel-validate --strict
```
## Validation Checks (10 categorias)
### 1. Check Dockerfile
```bash
# Lint com hadolint (Docker best practices)
hadolint Dockerfile
```
**Checks:**
- ✅ Multi-stage build (>= 2 stages)
- ✅ FROM com versão específica (not :latest)
- ✅ HEALTHCHECK presente
- ✅ USER non-root
- ✅ EXPOSE port
- ⚠️ Warnings: npm install (usar npm ci), COPY . antes package.json
**Auto-fix (--fix):**
- Adicionar USER nodejs se ausente
- Adicionar HEALTHCHECK se ausente
- Sugerir multi-stage (não altera automaticamente)
### 2. Check .dockerignore
**Checks:**
- ✅ Existe
- ✅ Espelha .gitignore (node_modules, dist, .env)
- ❌ Se não existe → --fix: criar automaticamente
**Auto-fix (--fix):**
```
node_modules
dist
build
.env
.env.local
*.log
.git
.DS_Store
```
### 3. Check docker-compose.yml
**Checks:**
- ✅ Traefik labels presentes
- ✅ router.rule com domain
- ✅ service.loadbalancer.server.port match Dockerfile EXPOSE
- ✅ healthcheck.path configurado (/health)
- ⚠️ Warnings: certresolver não é letsencrypt, restart policy não definida
**Auto-fix (--fix):**
- Adicionar restart: unless-stopped
- Corrigir sintaxe de labels Traefik
### 4. Check package.json (Node.js)
**Checks:**
- ✅ Scripts: build, start, lint, test
- ✅ Dependencies: express/fastify (API framework)
- ⚠️ devDependencies em dependencies (bloat)
### 5. Check Health Endpoint
**Checks:**
- ✅ Ficheiro existe (src/routes/health.ts ou similar)
- ✅ GET /health implementado
- ❌ Se não existe → --fix: criar template
**Template Health Endpoint (Node.js/Express):**
```typescript
import { Router } from 'express'
const router = Router()
router.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
})
})
export default router
```
### 6. Check .gitignore
**Checks:**
- ✅ node_modules
- ✅ dist/build
- ✅ .env (não .env.example)
- ✅ logs/
### 7. Check .env.example
**Checks:**
- ✅ Existe (template para .env)
- ✅ Todas vars documentadas
- ⚠️ Valores default sensíveis (passwords)
### 8. Check CI/CD
**Checks:**
- ✅ .gitea/workflows/deploy.yml existe
- ✅ Webhook URL configurado (secret)
- ⚠️ Tests não rodando (npm test ausente)
**Auto-fix (--fix):**
- Criar .gitea/workflows/deploy.yml usando template
### 9. Test Local Build
```bash
docker build --no-cache -t test:validate .
```
**Checks:**
- Build completa sem erros
- Image size < 200MB (Node.js)
- Image size > 200MB (optimizar multi-stage)
### 10. Validar Serviço via API (se já deployed)
```bash
# Obter token
TOKEN=$(cat /etc/easypanel/.api-token)
# Verificar estado do serviço existente
curl -s "http://localhost:3000/api/trpc/services.app.inspectService?input=$(echo -n '{"json":{"projectName":"PROJECT","serviceName":"SERVICE"}}' | jq -sRr @uri)" \
-H "Authorization: Bearer $TOKEN" | jq '.result.data.json'
# Verificar estatisticas de recursos
curl -s "http://localhost:3000/api/trpc/monitor.getSystemStats" \
-H "Authorization: Bearer $TOKEN"
```
### 11. Output Report
**Score: X/10**
- Cada categoria = 1 ponto
- Critical issues = 0 pontos
- Warnings = 0.5 pontos
## Output Format
```
🔍 EasyPanel Deploy Validation
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 SCORE: X/10 (EXCELLENT | GOOD | FAIR | POOR)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ PASSED (X)
1. Dockerfile: Multi-stage build
2. docker-compose.yml: Traefik configured
...
⚠️ WARNINGS (X)
1. Dockerfile: Use 'npm ci' instead of 'npm install'
2. docker-compose.yml: Missing restart policy
...
❌ CRITICAL (X)
1. .dockerignore: FILE MISSING
→ Impact: Build includes unnecessary files
→ Fix: Create .dockerignore
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔧 AUTO-FIXES (--fix flag detected)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Created .dockerignore
✅ Updated docker-compose.yml: Added restart policy
✅ Dockerfile: Changed npm install → npm ci
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 RECOMMENDATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Add .gitea/workflows/deploy.yml for CI/CD
2. Implement integration tests
3. Consider Prometheus metrics endpoint (/metrics)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ READY TO DEPLOY (score >= 7/10)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -0,0 +1,123 @@
---
name: gateway-check
description: Gestao completa dos MCPs no gateway.descomplicar.pt — health check, restart, troubleshoot, mapa de portas, adicionar MCPs. Usar quando MCP falha, health check, ou gestao gateway.
context: fork
layer: wiki
---
# /gateway-check v2.0
Gestao e health check dos MCPs no servidor gateway (VM 103, gateway.descomplicar.pt).
**Referencia:** Memory `mcp-gateway.md` | PROC-MCP-Desenvolvimento.md
---
## Acesso
- **VM:** 103 no Proxmox (QEMU)
- **IP:** 5.9.90.69
- **SSH:** `mcp__ssh-unified__ssh_execute(server="gateway")`
- **HTTPS:** `https://gateway.descomplicar.pt/v1/<nome>/mcp`
- **Nginx whitelist:** 188.251.199.30 (IP fixo NOS). Se 403 -> verificar IP com `curl -4 ifconfig.me`
- **Nginx config:** `/etc/nginx/sites-enabled/` no gateway
- **NAO confundir com:** server (VM 100, 5.9.90.105), easy (VM 101, 5.9.90.70), dev (LXC 102)
---
## Mapa de MCPs (30 services — actualizado 28-03-2026)
### pm2 (Node.js — /opt/mcp-gateway/)
| pm2 ID | Nome | Porta | nginx path | Prioridade |
|--------|------|-------|------------|------------|
| 0 | mcp-desk-crm | 3150 | /v1/desk-crm/mcp | P1 |
| 1 | mcp-memory | 3151 | /v1/memory/mcp | P1 |
| 2 | mcp-wikijs | 3152 | /v1/wikijs/mcp | P3 |
| 5 | mcp-moloni | 3158 | /v1/moloni/mcp | P2 |
| 6 | mcp-youtube-research | 3157 | /v1/youtube-research/mcp | P3 |
| 7 | mcp-youtube | 3187 | /v1/youtube/mcp | P3 |
### systemd (24 services)
| Service | Porta | nginx path | Tipo | Prioridade |
|---------|-------|------------|------|------------|
| google-workspace-mcp | 3164 | /v1/google-workspace/mcp | Python/FastMCP | P1 |
| mcp-time | 3163 | /v1/mcp-time/mcp | Node | P1 |
| imap-enterprise | 3160 | /v1/imap/mcp | Node | P2 |
| gitea-mcp | 3162 | /v1/gitea/mcp | Go | P2 |
| n8n-mcp | 3161 | /v1/n8n/mcp | Node | P2 |
| gsc-mcp | 3153 | /v1/gsc/mcp | Python/FastMCP | P2 |
| google-analytics-mcp | 3156 | /v1/google-analytics/mcp | Python/FastMCP | P2 |
| context7-mcp | 3159 | /v1/context7/mcp | Node | P2 |
| mcp-reonic | 3170 | /v1/reonic/mcp | Node | P3 |
| cloudflare-dns-mcp | 3171 | /v1/cloudflare-dns/mcp | supergateway | P3 |
| magic-mcp | 3172 | /v1/magic/mcp | supergateway | P3 |
| mcp-echarts-mcp | 3173 | /v1/mcp-echarts/mcp | supergateway | P3 |
| mcp-mermaid-mcp | 3174 | /v1/mcp-mermaid/mcp | supergateway | P3 |
| metabase-mcp | 3175 | /v1/metabase/mcp | supergateway | P3 |
| pixabay-mcp | 3176 | /v1/pixabay/mcp | supergateway | P3 |
| replicate-mcp | 3177 | /v1/replicate/mcp | supergateway | P3 |
| outline-api-mcp | 3178 | /v1/outline-api/mcp | supergateway | P3 |
| pexels-mcp | 3179 | /v1/pexels/mcp | supergateway | P3 |
| penpot-mcp | 3180 | /v1/penpot/mcp | supergateway | P3 |
| vimeo-mcp | 3181 | /v1/vimeo/mcp | supergateway | P3 |
| presenton-mcp | 3182 | /v1/presenton/mcp | supergateway | P3 |
| cwp-mcp | 3183 | /v1/cwp/mcp | supergateway | P3 |
| design-engine-mcp | 3184 | /v1/design-engine/mcp | supergateway | P3 |
**Prioridades:** P1=critico (bloqueia trabalho) | P2=importante (degrada workflow) | P3=util
**Proxima porta livre:** 3188
---
## Protocolo de Health Check
Executar via `mcp__ssh-unified__ssh_execute(server="gateway")` em 2 chamadas:
### Chamada 1 — estado geral
```bash
echo "=== PM2 ===" && pm2 list 2>/dev/null && echo "=== SYSTEMD ===" && systemctl list-units --type=service --state=running --no-pager | grep -i mcp && echo "=== FAILED ===" && systemctl list-units --type=service --state=failed --no-pager | grep -i mcp && echo "=== LOAD ===" && uptime && free -h
```
### Chamada 2 — portas + erros
```bash
echo "=== PORTAS ===" && for port in 3150 3151 3152 3153 3156 3157 3158 3159 3160 3161 3162 3163 3164 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3187; do if ss -tln | grep -q ":${port} "; then echo "OK :${port}"; else echo "DOWN :${port}"; fi; done && echo "=== RAM MCPs ===" && ps aux | grep -E 'mcp|supergateway' | grep -v grep | awk '{sum+=$6} END {printf "Total: %.0f MB\n", sum/1024}' && echo "=== ERROS (30min) ===" && for svc in $(systemctl list-units --type=service --state=running | grep -i mcp | awk '{print $1}'); do errs=$(journalctl -u $svc --since "30 min ago" -p err --no-pager -q 2>/dev/null | wc -l); if [ "$errs" -gt 0 ]; then echo "--- $svc ($errs) ---"; journalctl -u $svc --since "30 min ago" -p err --no-pager -q 2>/dev/null | tail -2; fi; done && echo "=== PM2 ERROS ===" && pm2 logs --err --lines 3 --nostream 2>/dev/null
```
### Output esperado
Apresentar como tabela resumo com data via mcp-time:
```
## Gateway Health — [data]
Servidor: gateway.descomplicar.pt | Load: X.XX | RAM: X.XG/XG | MCPs RAM: XXXMB
X/30 operacionais | Alertas: N
```
**Criterios:** OK=running+porta escuta | WARN=running com erros ou >5 restarts | DOWN=parado ou porta fechada
---
## Operacoes
### Restart de um MCP
```bash
# pm2
pm2 restart <nome>
# systemd
systemctl restart <nome>.service
```
### Ver logs de um MCP
```bash
# pm2
pm2 logs <nome> --lines 30 --nostream
# systemd
journalctl -u <nome>.ser
+8
View File
@@ -0,0 +1,8 @@
# skills
## Documentação
- [README](README.md)
- [AGENTS](AGENTS.md)
- [STATUS](STATUS.md)
- [MEMORY](MEMORY.md)
+177
View File
@@ -0,0 +1,177 @@
---
name: infra-check
description: Health check de MCPs e auditoria de despesas — sábado check completo 9 MCPs + auditoria despesas, domingo check resumido top 5 + despesas sem PDF.
context: fork
layer: wiki
---
# /infra-check v1.0
MCP Health Check + Auditoria de Despesas.
---
## MCPs por Prioridade
**P1 - Criticos (bloqueiam trabalho):**
- desk-crm-v3 (CRM)
- filesystem (ficheiros locais)
- mcp-time (data/hora)
**P2 - Importantes (degradam workflow):**
- google-workspace (email/calendar)
- ssh-unified (servidores)
- mem0 (memoria)
**P3 - Uteis:**
- gitea (repos)
- moloni (facturacao)
- dify-kb (knowledge base)
---
## Protocolo Sabado (Completo)
### 1. MCP Health Check (9 MCPs)
```
Para cada MCP, executar teste simples:
| MCP | Teste | Timeout |
|-----|-------|---------|
| desk-crm-v3 | get_tickets(limit=1) | 5s |
| filesystem | list_directory(~) | 2s |
| mcp-time | current_time | 2s |
| google-workspace | calendar_get_events(hoje) | 5s |
| ssh-unified | ssh_list_servers | 3s |
| mem0 | search_memories("test") | 5s |
| gitea | list_my_repos | 5s |
| moloni | getall (companies) | 5s |
| dify-kb | list_datasets | 5s |
Resultado por MCP:
- OK - respondeu em <2s
- LENTO - respondeu em >2s
- FALHA - timeout ou erro
```
### 2. Auditoria Despesas Completa
```
2a. DESPESAS SEM PDF (ultimos 60 dias):
mcp__ssh-unified__ssh_execute(server="desk", command="
mysql -u ealmeida -p'9qPRdCGGqM4o' ealmeida_desk24 -e \"
SELECT e.id, e.expense_name, e.amount, e.date, e.note,
(SELECT COUNT(*) FROM tblfiles f WHERE f.rel_id = e.id AND f.rel_type = 'expense') as pdfs
FROM tblexpenses e
WHERE e.id >= 770 AND e.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
HAVING pdfs = 0
ORDER BY e.date DESC;
\"
")
Excluir: AT (cat 15), Salarios (cat 22), SS (cat 25) - nao tem recibo
2b. DESPESAS SEM CATEGORIA:
WHERE id >= 770 AND (category = 0 OR category IS NULL)
2c. VALORES ANOMALOS (>500 EUR ou negativos):
WHERE id >= 770 AND (amount > 500 OR amount < 0)
2d. RECONCILIACAO MENSAL (apenas 1o sabado do mes, DAY(CURDATE()) <= 7):
- Contar despesas do mes anterior por categoria
- Comparar com mes homologo
- Alertar se variacao >30%
2e. FORNECEDORES RECORRENTES em falta:
Verificar se fornecedores mensais tem despesa este mes:
Anthropic, Cursor, Hetzner, Google One, ElasticEmail, Canva
Alertar se falta apos dia 10 do mes
```
### 3. Verificar Gateway
```
WebFetch("https://gateway.descomplicar.pt/health")
Se falha → alerta critico (todos os MCPs gateway afectados)
```
---
## Protocolo Domingo (Resumido)
### 1. MCP Health Check (top 5)
```
Apenas P1 + P2:
- desk-crm-v3, filesystem, mcp-time, google-workspace, ssh-unified
Teste rapido, timeout 5s
Apenas verificar se responde
```
### 2. Despesas sem PDF (30 dias)
```
Mesma query do sabado mas INTERVAL 30 DAY
SE >3 sem PDF (excl. AT/Salario/SS) → alertar
SE 0 → "Todas as despesas recentes tem documento"
```
---
## Output
```markdown
## MCP Health Check ([Sabado/Domingo])
[status] X/Y MCPs operacionais
[alertas por MCP com problema]
## Auditoria Despesas ([Sabado/Domingo])
### Despesas sem documento (N) [se sabado]
| # | Fornecedor | Valor | Data | Accao |
### Fornecedores recorrentes [se sabado]
- [Fornecedor]: OK ou FALTA
### Resumo mensal [se 1o sabado]
- Total [Mes]: X EUR (Y despesas)
- vs [Mes anterior]: +/-Z%
[ou versao resumida se domingo]
```
---
## Troubleshooting Automatico
```
Se MCP falha:
1. Verificar gateway: WebFetch("https://gateway.descomplicar.pt/health")
2. Se gateway OK mas MCP falha → problema no MCP especifico → documentar
3. Se gateway falha → problema de rede/servidor mcp-hub → alerta critico
```
---
## Anti-Patterns
- NUNCA executar health check em dias uteis (reservado para Sab/Dom via /today)
- NUNCA ignorar falha de MCP P1 (critico)
- SEMPRE incluir accao sugerida para cada problema encontrado
---
*Skill v1.0.0 | 04-03-2026 | Descomplicar®*
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+1
View File
@@ -0,0 +1 @@
## 2026-06-30 init | Estrutura padrão
+147
View File
@@ -0,0 +1,147 @@
---
name: mcp-dev
description: Desenvolvimento e teste de servidores MCP — criação, configuração e gestão de MCPs customizados seguindo os padrões Descomplicar com transporte StreamableHTTP.
layer: wiki
---
# /mcp-dev - Desenvolvimento de MCPs
Skill para criação, configuração e gestão de servidores MCP customizados.
## Contexto NotebookLM
Consultar ANTES de executar:
```
mcp__notebooklm__notebook_query({
notebook_id: "73102308-70ef-403e-9be9-eae0cfc62d55",
query: "<adaptar ao contexto do pedido>"
})
```
**Procedimentos obrigatórios — consultar antes de criar/modificar MCPs:**
- [PROC-MCP-Desenvolvimento.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Desenvolvimento.md) — Guia oficial v2.4
- [PROC-MCP-Troubleshooting-Erro-471.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Troubleshooting-Erro-471.md)
- [PROC-MCP-Google-Auth.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Google-Auth.md)
> **Regra #48:** Novos MCPs são desenvolvidos no **container dev** (`server:"dev"`, path `/root/Dev/<nome-mcp>`). Após validação, mover para `/home/ealmeida/mcp-servers/` em produção.
---
## Comandos
| Comando | Descrição |
|---------|-----------|
| `/mcp-dev create <nome>` | Criar scaffold TypeScript completo |
| `/mcp-dev config <nome>` | Configurar MCP em ~/.claude.json |
| `/mcp-dev test <nome>` | Testar conexão e listar ferramentas |
| `/mcp-dev eval <nome>` | Executar evaluations do MCP |
| `/mcp-dev docs <nome>` | Gerar documentação Obsidian |
| `/mcp-dev list` | Listar MCPs em ~/mcp-servers/ |
| `/mcp-dev status` | Estado dos MCPs activos na sessão |
---
## Workflow de 4 Fases
### Fase 1: Research
1. Consultar NotebookLM (notebook acima)
2. Verificar MCPs existentes — evitar duplicação
3. Identificar ferramentas necessárias e nomear segundo padrão
4. Definir transporte: HTTP (recomendado) ou stdio
5. Criar spike/PoC se tecnologia desconhecida
**Nomenclatura de tools:** `{serviço}_{acção}_{recurso}` em snake_case, máx. 40 caracteres.
```
# Correcto
get_customer_notes, create_project_task, list_invoice_items
# Errado
getCustomerNotes (camelCase), get_customer_notes_with_all_billing_details (>40 chars)
```
### Fase 2: Implementation
1. Scaffold no container dev: `/root/Dev/<nome-mcp>/`
2. Implementar tools com **annotations obrigatórias** e validação Zod
3. Definir capabilities completas (tools + resources + prompts)
4. Responses em **dual format**: Markdown (text) + JSON (structuredContent)
5. Erros **accionáveis**: causa + próximos passos
**Annotations obrigatórias em cada tool:**
```typescript
annotations: {
readOnlyHint: true, // não modifica estado
destructiveHint: false, // não é destrutiva
idempotentHint: true, // mesmo resultado em chamadas repetidas
openWorldHint: false, // opera em sistema fechado (interno)
}
```
Usar `inferAnnotations()` do SDK para inferência automática:
```typescript
import { inferAnnotations } from '@modelcontextprotocol/sdk/server/utils.js';
const annotated = inferAnnotations(tool);
```
**Capabilities — sempre completas (anti-erro 471):**
```typescript
capabilities: { tools: {}, resources: {}, prompts: {} }
```
**Dual format de resposta:**
```typescript
return {
content: [{ type: 'text', text: `# Cliente ${d.name}\n**Email:** ${d.email}` }],
structuredContent: { id: d.id, name: d.name, email: d.email }
};
```
**Erros accionáveis:**
```typescript
return {
content: [{
type: 'text',
text: [
`Erro ao obter cliente ID ${id}.`,
`Causa: ${error.message}`,
`Verificar: 1) ID existe 2) Permissões 3) Conexão BD`
].join('\n')
}],
isError: true
};
```
### Fase 3: Review
1. Checklist de segurança (ver `references/best-practices.md`)
2. `pnpm audit` — sem vulnerabilidades críticas
3. Validar limites de nomes de tools (máx. 40 chars)
4. Testar build: `npm run build`
5. Testar manualmente: `echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/index.js`
6. Documentar decisões em ADR se necessário
**Grep de validação rápida:**
```bash
grep -rn '`.*\${.*}`' src/ | grep -i 'select\|insert\|update\|delete'
grep -rn 'Math.random' src/
```
### Fase 4: Evaluations
Criar e executar testes estruturados antes do deploy.
**10 evaluations obrigatórias por MCP** (ver `references/evaluation-guide.md`):
| # | Pergunta | Cenário |
|---|----------|---------|
| 1 | Tool principal funciona com input válido? | Input mínimo válido |
| 2 | Input inválido retorna erro accionável? | Parâmetro obrigatório em falta |
| 3 | Recurso inexistente retorna erro claro? | ID=99999 |
| 4 | Operações idempotentes são estáveis? | Chamada repetida com mesmos params |
| 5 | Operações destrutivas apagam correctamente? | Delete em recurso existente |
| 6 | Tools read-only não alteram estado? | Get + verificar estado antes/depois |
| 7 | Paginação funciona com listas grandes? | 1000+ registos, página 2 |
| 8 | Auth invá
@@ -0,0 +1,287 @@
# MCP Best Practices - Referência Completa
> Extraído de auditorias a 27+ projectos MCP (500+ ferramentas).
> Ver também: [PROC-MCP-Desenvolvimento.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Desenvolvimento.md)
---
## Nomenclatura de Tools
**Padrão:** `{serviço}_{acção}_{recurso}` em snake_case
```
# Correcto
get_customer_notes
create_project_task
list_invoice_items
delete_session_token
# Errado
getCustomerNotes (camelCase)
customer-notes-get (kebab-case)
get_customer_notes_with_all_billing_details_and_history (>40 chars)
```
**Limites obrigatórios:**
- Tool name: ≤ 40 caracteres
- Total com prefixo `mcp__<servidor>__<tool>`: ≤ 64 caracteres
**Validação em código:**
```typescript
function validateToolName(name: string): void {
if (name.length > 40) {
throw new Error(`Tool "${name}" excede limite de 40 chars (${name.length})`);
}
}
allTools.forEach(t => validateToolName(t.name));
```
---
## Annotations Obrigatórias
Cada tool deve declarar as suas annotations para orientar o modelo:
```typescript
{
name: 'get_customer',
description: 'Obtém dados de um cliente pelo ID',
annotations: {
readOnlyHint: true, // Não modifica estado
destructiveHint: false, // Não é destrutiva
idempotentHint: true, // Mesmo resultado em chamadas repetidas
openWorldHint: false, // Opera em dados internos (fechado)
},
inputSchema: { ... }
}
```
**Referência rápida:**
| Annotation | Tipo | Significado |
|------------|------|-------------|
| `readOnlyHint` | boolean | Leitura sem efeitos secundários |
| `destructiveHint` | boolean | Pode apagar/substituir dados |
| `idempotentHint` | boolean | Resultado idêntico em múltiplas chamadas |
| `openWorldHint` | boolean | Interage com sistemas externos/web |
**Inferência automática com `inferAnnotations()`:**
```typescript
import { inferAnnotations } from '@modelcontextprotocol/sdk/server/utils.js';
// Inferir automaticamente a partir do nome e descrição da tool
const annotated = inferAnnotations(tool);
```
---
## Capabilities Obrigatórias (Regra de Ouro)
```typescript
// ERRADO: capabilities incompletas -> erro 471
capabilities: { tools: {} }
// CORRECTO: sempre declarar as três, mesmo vazias
capabilities: {
tools: {},
resources: {},
prompts: {}
}
```
**Handlers mínimos obrigatórios:**
```typescript
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...] }));
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
server.setRequestHandler(CallToolRequestSchema, async (req) => { ... });
```
---
## Formatos de Resposta
### Dual Format (JSON + Markdown)
Para máxima compatibilidade, devolver ambos:
```typescript
return {
content: [
{
type: 'text',
text: `# Cliente ${data.name}\n\n**ID:** ${data.id}\n**Email:** ${data.email}`
}
],
structuredContent: {
id: data.id,
name: data.name,
email: data.email,
status: data.status
}
};
```
### Erros Accionáveis
```typescript
// ERRADO: mensagem genérica
throw new Error('Falha ao obter dados');
// CORRECTO: mensagem accionável com contexto e próximos passos
return {
content: [{
type: 'text',
text: [
`Erro ao obter cliente ID ${customerId}.`,
`Causa: ${error.message}`,
`Verificar: 1) ID existe na BD 2) Permissões de acesso 3) Conexão à BD`
].join('\n')
}],
isError: true
};
```
---
## Validação com Zod
```typescript
import { z } from 'zod';
const GetCustomerSchema = z.object({
customer_id: z.number().int().positive(),
include_invoices: z.boolean().optional().default(false),
date_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
});
// No handler
const validated = GetCustomerSchema.parse(args);
// Zod lança ZodError automaticamente se inválido
```
---
## Error Handling
```typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(name, args);
return result;
} catch (error) {
if (error instanceof z.ZodError) {
return {
content: [{
type: 'text',
text: `Parâmetros inválidos:\n${error.errors.map(e => ` - ${e.path.join('.')}: ${e.message}`).join('\n')}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `Erro: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
```
---
## Logging
```typescript
// SEMPRE usar console.error (não console.log — interfere com stdio)
console.error(`[MCP:${toolName}] Início — params: ${JSON.stringify(args)}`);
console.error(`[MCP:${toolName}] Concluído em ${duration}ms`);
```
---
## Segurança (Checklist Pré-Commit)
- [ ] SQL injection: inputs validados antes de entrar em queries?
- [ ] Interpolação directa em SQL proibida sem validação
- [ ] Transacções em operações multi-query relacionadas
- [ ] Recursos (pool.connect) têm `finally` com release
- [ ] Cleanup (ROLLBACK) tem try-catch próprio
- [ ] `crypto.randomBytes()` em vez de `Math.random()`
- [ ] Secrets em variáveis de ambiente, nunca hardcoded
- [ ] `pnpm audit` sem vulnerabilidades críticas
**Grep de validação:**
```bash
# Interpolação SQL perigosa
grep -rn '`.*\${.*}`' src/ | grep -i 'select\|insert\|update\|delete\|order'
# Math.random em produção
grep -rn 'Math.random' src/
# connect() sem finally
grep -rn '\.connect()' src/
```
---
## Padrões SQL (Perfex CRM)
```typescript
// Perfex usa 0 como "não definido", NÃO NULL
client_id || 0,
project_id || 0,
// Excepção: source em leads precisa ID válido
const [defaultSource] = await db.query(
'SELECT id FROM tblleads_sources ORDER BY id ASC LIMIT 1'
);
leadSource = source || defaultSource?.id || 1;
```
**Tabelas Perfex (prefixo tbl):**
```
tblclients, tblprojects, tbltasks, tblinvoices,
tblleads, tblleads_sources, tblstaff, tbltaskstimers, tblexpenses
```
---
## Transportes
| Transporte | Estado | Uso |
|------------|--------|-----|
| StreamableHTTP | Recomendado | Novos MCPs, acesso remoto |
| stdio | Válido | Claude Code local, scripts |
| SSE | Deprecated | Retrocompatibilidade apenas |
**Portas HTTP reservadas (3200+):**
| Porta | MCP |
|-------|-----|
| 3200 | outline-postgresql |
| 3201+ | disponíveis |
**Portas SSE reservadas (3100+):**
| Porta | MCP |
|-------|-----|
| 3100 | desk-crm-v3 |
| 3101 | mem0 |
| 3102 | wikijs |
| 3103 | ssh-unified |
| 3105 | n8n |
| 3106 | cwp |
| 3107 | youtube-research |
| 3108 | moloni |
| 3109+ | disponíveis |
---
*best-practices.md v2.0 | 2026-03-10*
@@ -0,0 +1,280 @@
# MCP Evaluation Guide
> Guia para criar e executar evaluations de servidores MCP.
> Baseado nos padrões do mcp-builder Anthropic.
---
## O que são Evaluations MCP?
Evaluations são testes estruturados que validam o comportamento de um MCP em cenários reais. Não testam apenas se as ferramentas existem — testam se produzem **resultados correctos, seguros e previsíveis**.
**Objectivos:**
- Garantir que cada tool faz o que a descrição promete
- Detectar regressões após alterações
- Validar edge cases e erros esperados
- Confirmar que annotations correspondem ao comportamento real
---
## Estrutura de uma Evaluation
Cada evaluation é definida em XML e contém:
```xml
<evaluation>
<name>get_customer_basic</name>
<description>Verifica que get_customer retorna dados completos de um cliente existente</description>
<tool>get_customer</tool>
<input>
<customer_id>1</customer_id>
</input>
<expected>
<contains>name</contains>
<contains>email</contains>
<not_error>true</not_error>
</expected>
</evaluation>
```
---
## 10 Perguntas de Evaluation por MCP
Para cada MCP novo, responder a estas 10 perguntas criando um teste para cada uma:
```xml
<!-- 1. A tool mais básica funciona? -->
<evaluation id="1">
<question>A tool {principal} retorna dados quando recebe input válido?</question>
<scenario>Input mínimo válido</scenario>
<assert>Resposta não é erro, contém campos esperados</assert>
</evaluation>
<!-- 2. Erros de input são tratados graciosamente? -->
<evaluation id="2">
<question>O que acontece com input inválido (ID negativo, campo obrigatório em falta)?</question>
<scenario>Input inválido ou em falta</scenario>
<assert>Resposta tem isError=true, mensagem é accionável</assert>
</evaluation>
<!-- 3. O recurso inexistente é tratado correctamente? -->
<evaluation id="3">
<question>O que retorna quando o recurso pedido não existe (ID=99999)?</question>
<scenario>Recurso não encontrado</scenario>
<assert>Mensagem clara "não encontrado", não retorna null silencioso</assert>
</evaluation>
<!-- 4. Operações de escrita são idempotentes quando declaradas? -->
<evaluation id="4">
<question>Operações com idempotentHint=true produzem o mesmo resultado em chamadas repetidas?</question>
<scenario>Chamar a mesma tool create/update duas vezes com os mesmos parâmetros</scenario>
<assert>Segundo resultado idêntico ao primeiro ou erro controlado</assert>
</evaluation>
<!-- 5. Operações destrutivas têm confirmação ou são irreversíveis como declarado? -->
<evaluation id="5">
<question>Tools com destructiveHint=true apagam realmente dados? O utilizador é avisado?</question>
<scenario>Executar delete em recurso existente</scenario>
<assert>Dados apagados. Mensagem confirma acção irreversível</assert>
</evaluation>
<!-- 6. Tools read-only não alteram estado? -->
<evaluation id="6">
<question>Tools com readOnlyHint=true não modificam dados?</question>
<scenario>Executar get/list e verificar estado antes e depois</scenario>
<assert>Estado da BD/sistema idêntico antes e depois da chamada</assert>
</evaluation>
<!-- 7. Paginação e listas grandes funcionam? -->
<evaluation id="7">
<question>Tools de listagem com muitos resultados retornam dados paginados correctamente?</question>
<scenario>Lista com 1000+ registos, pedir página 2</scenario>
<assert>Retorna subset correcto, metadados de paginação presentes</assert>
</evaluation>
<!-- 8. Autenticação/autorização é validada? -->
<evaluation id="8">
<question>Chamadas sem credenciais válidas são rejeitadas?</question>
<scenario>Remover variável de ambiente com API key e chamar tool</scenario>
<assert>Erro claro de autenticação, não crash do servidor</assert>
</evaluation>
<!-- 9. structuredContent é consistente com content textual? -->
<evaluation id="9">
<question>O campo structuredContent contém os mesmos dados que o texto Markdown?</question>
<scenario>Comparar campos em structuredContent com conteúdo do text</scenario>
<assert>Dados idênticos em ambos os formatos</assert>
</evaluation>
<!-- 10. Performance é aceitável? -->
<evaluation id="10">
<question>A tool responde em menos de 5 segundos em condições normais?</question>
<scenario>Medir tempo de resposta em 10 chamadas consecutivas</scenario>
<assert>p95 < 5000ms, sem memory leaks após 100 chamadas</assert>
</evaluation>
```
---
## Como Executar Evaluations
### Método Manual (MCP Inspector)
```bash
# Instalar MCP Inspector
npx @modelcontextprotocol/inspector
# Conectar ao MCP local
# Interface web em http://localhost:5173
# Testar cada tool manualmente
```
### Método Automatizado (Script TypeScript)
```typescript
// eval/run-evals.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
interface EvalResult {
id: string;
passed: boolean;
duration: number;
error?: string;
}
async function runEval(
client: Client,
toolName: string,
input: Record<string, unknown>,
assertions: {
notError?: boolean;
containsFields?: string[];
isError?: boolean;
}
): Promise<EvalResult> {
const start = Date.now();
try {
const result = await client.callTool({ name: toolName, arguments: input });
const duration = Date.now() - start;
// Verificar assertions
if (assertions.notError && result.isError) {
return { id: toolName, passed: false, duration, error: 'Esperava sucesso, recebeu erro' };
}
if (assertions.isError && !result.isError) {
return { id: toolName, passed: false, duration, error: 'Esperava erro, recebeu sucesso' };
}
if (assertions.containsFields) {
const text = result.content[0]?.text || '';
for (const field of assertions.containsFields) {
if (!text.includes(field)) {
return { id: toolName, passed: false, duration, error: `Campo "${field}" não encontrado` };
}
}
}
return { id: toolName, passed: true, duration };
} catch (err) {
return {
id: toolName,
passed: false,
duration: Date.now() - start,
error: err instanceof Error ? err.message : String(err)
};
}
}
// Executar todas as evaluations
async function main() {
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/index.js']
});
const client = new Client({ name: 'eval-client', version: '1.0.0' });
await client.connect(transport);
const results: EvalResult[] = [];
// Eval 1: Tool básica
results.push(await runEval(client, 'get_customer', { customer_id: 1 }, {
notError: true,
containsFields: ['name', 'email']
}));
// Eval 2: Input inválido
results.push(await runEval(client, 'get_customer', { customer_id: -1 }, {
isError: true
}));
// Eval 3: Recurso inexistente
results.push(await runEval(client, 'get_customer', { customer_id: 99999 }, {
isError: true
}));
// Sumário
const passed = results.filter(r => r.passed).length;
const total = results.length;
console.log(`\nEvaluations: ${passed}/${total} passou`);
results.filter(r => !r.passed).forEach(r => {
console.log(` FALHOU ${r.id}: ${r.error}`);
});
await client.close();
process.exit(passed === total ? 0 : 1);
}
main().catch(console.error);
```
**Adicionar ao package.json:**
```json
{
"scripts": {
"eval": "tsx eval/run-evals.ts",
"eval:ci": "npm run build && npm run eval"
}
}
```
---
## Checklist de Evaluations (pré-deploy)
- [ ] Eval 1 passa: tool principal com input válido
- [ ] Eval 2 passa: input inválido retorna erro accionável
- [ ] Eval 3 passa: recurso inexistente retorna erro claro
- [ ] Eval 4 passa: idempotência verificada (se aplicável)
- [ ] Eval 5 passa: operações destrutivas confirmadas
- [ ] Eval 6 passa: read-only não altera estado
- [ ] Eval 7 passa: paginação funciona (se aplicável)
- [ ] Eval 8 passa: auth inválida rejeitada graciosamente
- [ ] Eval 9 passa: structuredContent consistente
- [ ] Eval 10 passa: p95 < 5000ms
---
## Integração CI/CD
```yaml
# .gitea/workflows/eval.yml
name: MCP Evaluations
on: [push, pull_request]
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run eval:ci
```
---
*evaluation-guide.md v1.0 | 2026-03-10*
@@ -0,0 +1,341 @@
# MCP Templates - Scaffolding TypeScript
> Templates de código prontos a usar. Substituir `<nome>` pelo nome real do MCP.
---
## package.json
```json
{
"name": "mcp-<nome>",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"start:http": "node dist/index-http.js",
"dev": "tsx src/index.ts",
"dev:http": "tsx src/index-http.ts",
"eval": "tsx eval/run-evals.ts",
"eval:ci": "npm run build && npm run eval",
"reload:http": "npm run build && systemctl --user restart mcp-<nome> && sleep 2 && curl -s http://127.0.0.1:32XX/health"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"tsx": "^4.0.0"
}
}
```
---
## tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "eval"]
}
```
---
## src/index.ts (stdio — padrão local)
```typescript
#!/usr/bin/env node
/**
* MCP <Nome>
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
// --- Schemas de validação ---
const ExampleSchema = z.object({
param: z.string().min(1, 'param é obrigatório'),
});
// --- Definição de tools ---
const allTools = [
{
name: 'example_get_item', // {serviço}_{acção}_{recurso}
description: 'Obtém um item pelo ID',
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
inputSchema: {
type: 'object' as const,
properties: {
param: { type: 'string', description: 'ID do item' },
},
required: ['param'],
},
handler: async (args: unknown) => {
const { param } = ExampleSchema.parse(args);
// ... lógica aqui
return {
content: [{ type: 'text' as const, text: `Item: ${param}` }],
structuredContent: { id: param, status: 'found' },
};
},
},
];
// --- Servidor ---
const server = new Server(
{ name: 'mcp-<nome>', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: allTools.map(t => ({
name: t.name,
description: t.description,
annotations: t.annotations,
inputSchema: t.inputSchema,
})),
}));
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = allTools.find(t => t.name === name);
if (!tool) {
return {
content: [{ type: 'text', text: `Tool desconhecida: ${name}` }],
isError: true,
};
}
try {
return await tool.handler(args);
} catch (error) {
if (error instanceof z.ZodError) {
return {
content: [{
type: 'text',
text: `Parâmetros inválidos:\n${error.errors.map(e => ` - ${e.path.join('.')}: ${e.message}`).join('\n')}`,
}],
isError: true,
};
}
return {
content: [{
type: 'text',
text: `Erro em ${name}: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP <nome> running on stdio');
}
main().catch(console.error);
```
---
## src/index-http.ts (StreamableHTTP — padrão remoto)
```typescript
#!/usr/bin/env node
/**
* MCP <Nome> - HTTP Server Mode
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import * as http from 'http';
import { URL } from 'url';
import { randomUUID } from 'crypto';
const PORT = parseInt(process.env.MCP_HTTP_PORT || '3200', 10);
const HOST = process.env.MCP_HTTP_HOST || '127.0.0.1';
const STATEFUL = process.env.MCP_STATEFUL !== 'false';
const sessions = new Map<string, { transport: StreamableHTTPServerTransport }>();
// Importar allTools do mesmo local que stdio
import { allTools } from './tools/index.js';
function createMcpServer(): Server {
const server = new Server(
{ name: 'mcp-<nome>', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: allTools.map(t => ({
name: t.name,
description: t.description,
annotations: t.annotations,
inputSchema: t.inputSchema,
})),
}));
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = allTools.find(t => t.name === name);
if (!tool) {
return { content: [{ type: 'text', text: `Tool desconhecida: ${name}` }], isError: true };
}
try {
return await tool.handler(args);
} catch (error) {
return {
content: [{ type: 'text', text: `Erro: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
});
return server;
}
async function main() {
const httpServer = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
const url = new URL(req.url || '/', `http://${HOST}:${PORT}`);
if (url.pathname === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', version: '1.0.0', sessions: sessions.size, tools: allTools.length }));
return;
}
if (url.pathname === '/mcp') {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: STATEFUL ? () => randomUUID() : undefined,
});
const srv = createMcpServer();
if (STATEFUL && transport.sessionId) {
sessions.set(transport.sessionId, { transport });
transport.onclose = () => { if (transport.sessionId) sessions.delete(transport.sessionId); };
}
await srv.connect(transport);
await transport.handleRequest(req, res);
} catch (error) {
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error' }));
}
}
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
});
httpServer.listen(PORT, HOST, () => {
console.log(`MCP <nome> HTTP v1.0.0`);
console.log(` Endpoint: http://${HOST}:${PORT}/mcp`);
console.log(` Health: http://${HOST}:${PORT}/health`);
console.log(` Mode: ${STATEFUL ? 'Stateful' : 'Stateless'}`);
});
const shutdown = () => httpServer.close(() => process.exit(0));
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
main().catch(console.error);
```
---
## ~/.config/systemd/user/mcp-<nome>.service
```ini
[Unit]
Description=MCP <Nome> HTTP Server
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/ealmeida/mcp-servers/mcp-<nome>
ExecStart=/home/ealmeida/.nvm/versions/node/v22.18.0/bin/node dist/index-http.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=MCP_HTTP_PORT=32XX
Environment=LOG_LEVEL=error
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
```
---
## Configuração ~/.claude.json
```json
{
"mcpServers": {
"<nome>": {
"type": "http",
"url": "http://127.0.0.1:32XX/mcp"
}
}
}
```
---
*templates.md v1.0 | 2026-03-10*
+152
View File
@@ -0,0 +1,152 @@
---
name: pbs-config
description: Configuracao do Proxmox Backup Server (PBS) -- criacao de datastores, politicas de retencao, sync jobs e remote targets para estrategia 3-2-1.
layer: wiki
---
# PBS Config
Configuracao completa de Proxmox Backup Server (PBS) com datastores, politicas de retencao, sync jobs e estrategia de backup 3-2-1.
## Quando Usar
- Configurar PBS apos instalacao Proxmox
- Criar datastores para backups
- Definir retention policies (7 daily, 4 weekly, 6 monthly)
- Configurar remote sync entre nodes PBS
- Implementar estrategia 3-2-1 backup
## Sintaxe
```bash
/pbs-config <datastore-path> [--retention 7:4:6] [--remote-sync node2] [--dedup on]
```
## Exemplos
```bash
# PBS basico com retention padrao
/pbs-config /mnt/pbs-datastore
# PBS com retention custom e remote sync
/pbs-config /mnt/pbs-datastore --retention 10:5:12 --remote-sync pbs-node2.descomplicar.pt
# PBS sem deduplicacao (se storage nao suporta)
/pbs-config /mnt/pbs-main --dedup off
```
## Knowledge Sources (Consultar SEMPRE)
### NotebookLM Proxmox Research
```bash
mcp__notebooklm__notebook_query \
notebook_id:"276ccdde-6b95-42a3-ad96-4e64d64c8d52" \
query:"proxmox backup server pbs datastore retention deduplication"
```
### Hub Docs
- Hub/05-Projectos/Cluster Descomplicar/Research/Proxmox-VE/Guia-Definitivo-Proxmox-Hetzner.md
- Modulo 3: Storage e Backups (PBS, estrategia 3-2-1, deduplicacao)
## Workflow Resumo
O setup completo segue 8 fases. Detalhes em `references/pbs-setup-completo.md`.
### Fases
| Fase | Descricao | Comandos-chave |
|------|-----------|----------------|
| 1 | Instalacao PBS | `apt install proxmox-backup-server` |
| 2 | Criar Datastore | `proxmox-backup-manager datastore create` |
| 3 | Users e Permissions | `proxmox-backup-manager user create` |
| 4 | Integrar PVE com PBS | `pvesm add pbs` |
| 5 | Backup Jobs | `vzdump --storage pbs-main` |
| 6 | Remote Sync (cluster) | `proxmox-backup-manager sync-job create` |
| 7 | Monitoring | `proxmox-backup-manager datastore status` |
| 8 | Teste Restore | `qmrestore pbs-main:backup/...` |
### Comandos Essenciais
```bash
# Criar datastore
proxmox-backup-manager datastore create main-store /mnt/pbs-datastore
# Retention padrao (7d/4w/6m/3y)
proxmox-backup-manager datastore update main-store \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6 --keep-yearly 3
# User dedicado
proxmox-backup-manager user create pve-backup@pbs --email admin@descomplicar.pt
# Adicionar PBS ao PVE
pvesm add pbs pbs-main --server SERVER_IP --datastore main-store --username pve-backup@pbs
# Backup diario
vzdump --storage pbs-main --mode snapshot --compress zstd --all 1
# Remote sync
proxmox-backup-manager sync-job create sync-from-node-b \
--remote pbs-node-b --remote-store main-store --store secondary-store \
--schedule "0 3 * * *" --remove-vanished true
# Garbage collection
proxmox-backup-manager garbage-collection start main-store
# Verificacao integridade
proxmox-backup-manager verify-job create verify-main \
--store main-store --schedule "0 4 * * 0"
```
## Estrategia 3-2-1
| Componente | Implementacao |
|------------|---------------|
| 3 copias | VMs producao + PBS Node B + PBS Node A sync |
| 2 medias | NVMe (producao) + HDD Enterprise (PBS) |
| 1 off-site | Hetzner Storage Box / S3 / PBS externo |
| RPO | 1h (hourly se critico) |
| RTO | 2-4h (restore + validacao) |
## Troubleshooting Rapido
| Problema | Solucao |
|----------|---------|
| "no space" | `garbage-collection start` + reduzir retention |
| Remote sync falha | Verificar conectividade + auth + `journalctl -u proxmox-backup` |
| Dedup ratio baixo | VMs encrypted nao deduplica; verificar chunk size |
Detalhes completos em `references/pbs-avancado.md`.
## Quando NAO Usar
- Para backups ad-hoc manuais (usar vzdump directo)
- Para PBS ja configurado (usar troubleshooting guides)
- Para restore procedures (criar skill especifica se necessario)
## References
- **NotebookLM:** 276ccdde-6b95-42a3-ad96-4e64d64c8d52
- **PBS Docs:** https://pbs.proxmox.com/docs/
- **Guia Hub:** Hub/05-Projectos/Cluster Descomplicar/Research/Proxmox-VE/Guia-Definitivo-Proxmox-Hetzner.md (Modulo 3)
- **Setup detalhado:** `references/pbs-setup-completo.md`
- **Avancado e troubleshooting:** `references/pbs-avancado.md`
## Metadata (Desk CRM Task #1712)
```
Projecto: Cluster Proxmox Descomplicar (#65)
Tarefa: Migracao Infraestrutura (#1712)
Tags: pbs, backup, retention, deduplication, sync
```
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,125 @@
# PBS Avancado - Funcionalidades e Troubleshooting
## Monitoring e Maintenance
### Verificar Deduplicacao
```bash
# Ver estatisticas datastore
proxmox-backup-manager datastore status main-store
# Ratio deduplicacao (tipico 1.3-2.5x)
```
### Garbage Collection
```bash
# Liberar espaco de backups removidos (retention)
proxmox-backup-manager garbage-collection start main-store
# Agendar GC semanal (Domingo 02:00)
# Via Web UI: Datastore -> main-store -> Prune & GC
```
### Verificar Disk Usage
```bash
df -h /mnt/pbs-datastore
# ZFS
zfs list -o name,used,available,refer rpool/pbs-datastore
```
### Alertas Email
```bash
# Configurar notificacoes
# Via Web UI: Configuration -> Notifications
# SMTP server: mail.descomplicar.pt
# Alertas: disk usage >80%, backup failures
```
## PBS Advanced Features
### Verification Jobs
```bash
# Verificar integridade backups
proxmox-backup-manager verify-job create verify-main \
--store main-store \
--schedule "0 4 * * 0" # Domingo 04:00
```
### Namespace Organization
```bash
# Organizar backups por tipo
proxmox-backup-manager namespace create main-store/production
proxmox-backup-manager namespace create main-store/testing
proxmox-backup-manager namespace create main-store/archived
```
### Tape Backup (futuro)
- PBS suporta LTO tape
- Para compliance de longo prazo
- Cold storage
## Troubleshooting
### Backup failing: "no space"
```bash
# Verificar disk usage
df -h /mnt/pbs-datastore
# Run GC manual
proxmox-backup-manager garbage-collection start main-store
# Ajustar retention (reduzir keeps)
proxmox-backup-manager datastore update main-store --keep-daily 5
```
### Remote sync not working
```bash
# Verificar conectividade
ping <remote-pbs-ip>
# Testar autenticacao
curl -k https://<remote-pbs-ip>:8007/api2/json/access/ticket \
-d "username=pve-backup@pbs&password=<password>"
# Logs
journalctl -u proxmox-backup -f
```
### Dedup ratio baixo (<1.2x)
```bash
# Verificar se VMs tem dados compressiveis
# VMs com random data (encrypted) nao deduplica bem
# Verificar chunk size (padrao 4MB adequado)
proxmox-backup-manager datastore show main-store
```
## Estrategia 3-2-1 Backup
**Implementacao para Cluster Descomplicar:**
**3 copias:**
1. **Original:** VMs em Node A (producao)
2. **Backup primario:** PBS Node B (16TB HDD)
3. **Backup secundario:** PBS Node A remote sync (12TB HDD)
**2 medias diferentes:**
1. NVMe (VMs producao)
2. HDD Enterprise (PBS datastores)
**1 off-site:**
- **Opcao A:** Hetzner Storage Box (rsync daily)
- **Opcao B:** S3-compatible (Wasabi/Backblaze)
- **Opcao C:** PBS em VPS externo
**RPO:** 1h (backups hourly se critical)
**RTO:** 2-4h (restore + validacao)
## Metricas Esperadas
| Metrica | Valor tipico |
|---------|-------------|
| Dedup ratio | 1.5-2.5x |
| Compression ratio | 1.3-1.8x |
| Backup speed | 100-300 MB/s |
| Restore RTO | 2-4h (para VM 100GB) |
@@ -0,0 +1,235 @@
# PBS Setup Completo - Referencia Detalhada
## Fase 1: PBS Installation
### 1.1 Verificar se PBS ja esta instalado
```bash
dpkg -l | grep proxmox-backup-server
# Se nao instalado:
apt update
apt install proxmox-backup-server
```
### 1.2 Aceder PBS Web UI
```
https://SERVER_IP:8007
User: root
Password: (root password do servidor)
```
### 1.3 Configuracao Inicial
- Hostname
- DNS servers
- Time zone (Europe/Lisbon)
## Fase 2: Datastore Creation
### 2.1 Preparar Storage
**Para ZFS (recomendado):**
```bash
# Ja criado em /proxmox-setup:
# zfs create rpool/pbs-datastore
# Verificar
zfs list | grep pbs-datastore
# Optimizar para backup workload
zfs set compression=lz4 rpool/pbs-datastore
zfs set dedup=off rpool/pbs-datastore # Dedup no PBS, nao no ZFS
zfs set recordsize=1M rpool/pbs-datastore # Large files
```
**Para ext4 (HDD 16TB):**
```bash
# Particionar HDD
parted /dev/sda mklabel gpt
parted /dev/sda mkpart primary ext4 0% 100%
# Formatar
mkfs.ext4 /dev/sda1
# Montar
mkdir -p /mnt/pbs-datastore
echo "/dev/sda1 /mnt/pbs-datastore ext4 defaults 0 2" >> /etc/fstab
mount -a
```
### 2.2 Criar Datastore via CLI
```bash
proxmox-backup-manager datastore create main-store /mnt/pbs-datastore
# Verificar
proxmox-backup-manager datastore list
```
### 2.3 Configurar Retention Policy
```bash
# 7 daily, 4 weekly, 6 monthly (padrao)
proxmox-backup-manager datastore update main-store \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 3
```
**Explicacao Retention:**
- `keep-daily 7`: Mantem 7 backups diarios
- `keep-weekly 4`: Mantem 4 backups semanais (1 por semana)
- `keep-monthly 6`: Mantem 6 backups mensais (1 por mes)
- `keep-yearly 3`: Mantem 3 backups anuais
**Gestao automatica:** PBS elimina backups antigos baseado nestas regras.
## Fase 3: PBS Users e Permissions
### 3.1 Criar User para PVE Backups
```bash
# User dedicado para Proxmox enviar backups
proxmox-backup-manager user create pve-backup@pbs \
--email admin@descomplicar.pt
# Password
proxmox-backup-manager user update pve-backup@pbs --password
# Atribuir permissoes no datastore
proxmox-backup-manager acl update /datastore/main-store \
--auth-id pve-backup@pbs \
--role DatastoreBackup
```
### 3.2 Criar API Token (para automacao)
```bash
# Token para scripts/Terraform
proxmox-backup-manager user token create pve-backup@pbs automation-token \
--output-format json
# Guardar token de forma segura
# Formato: pve-backup@pbs!automation-token=<secret>
```
## Fase 4: Configure PVE to Use PBS
### 4.1 Adicionar PBS Storage em Proxmox VE
Via Web UI (Datacenter -> Storage -> Add -> Proxmox Backup Server):
- ID: `pbs-main`
- Server: `SERVER_IP` (ou hostname se cluster)
- Datastore: `main-store`
- Username: `pve-backup@pbs`
- Password: (password criado)
- Fingerprint: (auto-detect)
Via CLI:
```bash
pvesm add pbs pbs-main \
--server SERVER_IP \
--datastore main-store \
--username pve-backup@pbs \
--password <password>
```
### 4.2 Verificar Conectividade
```bash
pvesm status | grep pbs-main
```
## Fase 5: Backup Jobs (PVE)
### 5.1 Criar Backup Job para VMs Criticas
Via Web UI (Datacenter -> Backup -> Add):
- Storage: `pbs-main`
- Schedule: Daily 01:00
- Mode: Snapshot (live backup)
- Compression: zstd
- Notification: email admin@descomplicar.pt
Via CLI:
```bash
# Backup diario de todas VMs as 01:00
vzdump --storage pbs-main --mode snapshot --compress zstd --all 1
```
### 5.2 Agendar via cron (alternativa)
```bash
# /etc/cron.d/pve-backup-critical
0 1 * * * root vzdump --storage pbs-main --vmid 100,101,102 --mode snapshot --compress zstd
```
### 5.3 Backup Selectivo
```bash
# VMs criticas: diario
# VMs secundarias: 3x semana (Seg, Qua, Sex)
0 1 * * 1,3,5 root vzdump --storage pbs-main --vmid 200,201,202 --mode snapshot --compress zstd
```
## Fase 6: Remote Sync (2-Node Cluster)
**Setup para cluster:** PBS em Node B (primary) + PBS em Node A (secondary)
### 6.1 Configurar Remote em PBS Secondary (Node A)
Via Web UI PBS Node A (Configuration -> Remote):
- Name: `pbs-node-b`
- Host: `<node-b-ip>` ou `cluster.descomplicar.pt`
- Port: 8007
- Auth ID: `pve-backup@pbs`
- Password: (password)
- Fingerprint: (auto-detect)
### 6.2 Criar Sync Job
Via Web UI PBS Node A (Configuration -> Sync Jobs -> Add):
- Remote: `pbs-node-b`
- Remote Datastore: `main-store`
- Local Datastore: `secondary-store`
- Schedule: Daily 03:00 (apos backups)
- Remove vanished: Yes (sync deletes)
Via CLI em Node A:
```bash
proxmox-backup-manager sync-job create sync-from-node-b \
--remote pbs-node-b \
--remote-store main-store \
--store secondary-store \
--schedule "0 3 * * *" \
--remove-vanished true
```
### 6.3 Testar Sync Manual
```bash
proxmox-backup-manager sync-job run sync-from-node-b
```
## Fase 7: Restore Procedures (Testing)
### 7.1 Restore VM Teste
Via Web UI PVE:
- Storage -> pbs-main -> Backups
- Seleccionar VM backup
- Restore -> New VM ID (999)
- Start apos restore
### 7.2 Restore via CLI
```bash
# Listar backups disponiveis
pbs-client list --repository pve-backup@pbs@SERVER_IP:main-store
# Restore VM 100
qmrestore pbs-main:backup/vm/100/YYYY-MM-DD... 999
```
### 7.3 Validar Restore
```bash
qm start 999
# Verificar VM boota correctamente
# Teste servico critical
# Shutdown e remover VM teste
qm stop 999 && qm destroy 999
```
**Critico:** Testar restore ANTES de considerar backup strategy operacional.
@@ -0,0 +1,241 @@
---
name: proxmox-cluster
description: Formação de cluster Proxmox com 2+ nodes — configuração de Corosync, Quorum e preparação para High Availability.
disable-model-invocation: true
layer: wiki
---
# Proxmox Cluster
Formar cluster Proxmox 2+ nodes com Corosync, Quorum e preparação para High Availability.
## Quando Usar
- Formar cluster 2-node após migration complete
- Adicionar node a cluster existente
- Configurar quorum e fencing
- Preparar para HA (skill /proxmox-ha)
## Sintaxe
```bash
/proxmox-cluster create --node-a <ip-hostname> --node-b <ip-hostname> [--cluster-name]
/proxmox-cluster join --node <ip> --cluster <existing-cluster-ip>
```
## Exemplos
```bash
# Criar cluster 2-node
/proxmox-cluster create --node-a server.descomplicar.pt --node-b cluster.descomplicar.pt --cluster-name descomplicar
# Adicionar 3º node
/proxmox-cluster join --node pve-node3.descomplicar.pt --cluster cluster.descomplicar.pt
```
## Knowledge Sources
### NotebookLM
```bash
mcp__notebooklm__notebook_query \
notebook_id:"276ccdde-6b95-42a3-ad96-4e64d64c8d52" \
query:"proxmox cluster corosync quorum pvecm ha"
```
## Workflow Completo
### Pre-Requisites
**1. Verificar Nodes Prontos**
```bash
# Ambos nodes devem ter:
- Proxmox VE 8.x instalado (/proxmox-setup)
- Networking configurado (NAT ou vSwitch)
- PBS configurado (/pbs-config)
- Mesma versão PVE
- Hostnames únicos
- Conectividade IP entre nodes
```
**2. Validar Conectividade**
```bash
# De Node A → Node B
ping -c 3 <node-b-ip>
ssh root@<node-b-ip> pveversion
# De Node B → Node A
ping -c 3 <node-a-ip>
ssh root@<node-a-ip> pveversion
```
**3. Sincronizar Time (CRITICAL)**
```bash
# Ambos nodes devem ter NTP configurado
timedatectl status
# Instalar chrony se necessário
apt install chrony
systemctl enable --now chronyd
```
**4. Backup Pre-Cluster**
```bash
# Backup configs de ambos nodes
tar -czf /tmp/pre-cluster-backup.tar.gz /etc/pve /etc/network
# Transfer para PBS
```
### Fase 1: Cluster Creation (Node B)
**1.1 Criar Cluster em Node B (Primeiro Node)**
```bash
# SSH to Node B (cluster.descomplicar.pt)
ssh root@<node-b-ip>
# Criar cluster
pvecm create descomplicar
# Verificar
pvecm status
# Expected output:
# Cluster information
# Name: descomplicar
# Nodes: 1
# Expected votes: 1
```
**1.2 Obter Cluster Join Info**
```bash
# Obter join information (para Node A)
pvecm nodes
# Anot IP e nome do cluster
```
### Fase 2: Join Node A ao Cluster
**2.1 Join Node A**
```bash
# SSH to Node A (server.descomplicar.pt)
ssh root@<node-a-ip>
# Join cluster (fornecer IP do Node B)
pvecm add <node-b-ip>
# Durante processo:
# - Solicita password root do Node B
# - Transfere configuração cluster
# - Copia /etc/pve/
# - Reinicia serviços cluster
# AGUARDAR ~2-5min
```
**2.2 Verificar Join Successful**
```bash
# Em Node A:
pvecm status
# Expected output:
# Nodes: 2
# Expected votes: 2
# Quorum: 2 (Active)
# Listar nodes
pvecm nodes
# Should show both nodes
```
**2.3 Verificar Replicação /etc/pve/**
```bash
# Em Node A:
ls -lah /etc/pve/
# Should see:
# - nodes/ (ambos nodes)
# - qemu-server/ (VMs)
# - lxc/ (containers)
# - storage.cfg (shared)
# Teste: Criar VM em Node A via Web UI
# Verificar aparece em Node B também
```
### Fase 3: Quorum Configuration
**3.1 Verificar Quorum Votes**
```bash
pvecm status | grep "Expected votes"
# 2-node cluster:
# Expected votes: 2
# Quorum: 2
# CRITICAL: Com 2 nodes, perder 1 node = perder quorum
```
**3.2 Configurar QDevice (Opcional - 2-node clusters)**
**Problema 2-node cluster:** Se 1 node falha, cluster perde quorum (não pode fazer alterações).
**Solução:** Adicionar QDevice externo (3º vote)
```bash
# Em VPS externo leve (ou Raspberry Pi):
apt install corosync-qnetd
# Em ambos PVE nodes:
apt install corosync-qdevice
# Configurar QDevice (em Node A ou B):
pvecm qdevice setup <qdevice-ip>
# Verificar
pvecm status
# Expected votes: 3 (2 nodes + 1 qdevice)
```
**Recomendação Cluster Descomplicar:**
- Iniciar sem QDevice (aceitar limitação 2-node)
- Adicionar QDevice futuro se necessário
- Ou adicionar 3º node físico
### Fase 4: Storage Configuration
**4.1 Configurar Shared Storage (Opcional)**
**Opções:**
- NFS share
- Ceph (mínimo 3 nodes)
- ZFS replication (não shared, mas sync)
**Para 2-node sem shared storage:**
- VMs ficam em local storage de cada node
- Live migration copia disk (mais lento mas funciona)
- HA usa storage replication ou aceita downtime de boot
**4.2 Configurar PBS como Shared**
```bash
# PBS já configurado (/pbs-config)
# Adicionar PBS storage em ambos nodes via Web UI
# Datacenter → Storage → Add → Proxmox Backup Server
# ID: pbs-main
# Server: <pbs-ip>
# Datastore: main-store
# Content: VZDump backup files
# Nodes: ALL
```
### Fase 5: Networking Validation
**5.1 Verificar Cluster Network**
```bash
# Verificar Corosync usa network correcta
cat /etc/pve/corosync.conf
# Deve usar IP management (não vSwitch)
# bindnetaddr: <management-subne
+187
View File
@@ -0,0 +1,187 @@
---
name: proxmox-ha
description: Configuracao de High Availability em cluster Proxmox -- HA Manager, fencing devices (STONITH) e failover automatico para VMs criticas.
layer: wiki
---
# Proxmox HA
Configurar High Availability (HA) em cluster Proxmox com HA Manager, fencing devices e failover automatico para VMs criticas.
## Quando Usar
- Configurar HA apos cluster formation (/proxmox-cluster)
- Proteger VMs criticas com failover automatico
- Configurar fencing devices (STONITH)
- Definir HA groups por criticidade
- Testar failover procedures
## Sintaxe
```bash
/proxmox-ha configure --critical-vms <vm-ids> [--fencing watchdog|ipmi] [--max-relocate 2]
```
## Knowledge Sources
```bash
mcp__notebooklm__notebook_query \
notebook_id:"276ccdde-6b95-42a3-ad96-4e64d64c8d52" \
query:"proxmox ha high availability fencing stonith failover"
```
---
## Pre-Requisitos
**1. Cluster formado:**
```bash
pvecm status
# Expected: Quorum: Active, Nodes: 2+ online
```
**2. Shared Storage ou Replication:**
- **Shared storage** (NFS, Ceph): HA ideal (failover <30s)
- **Sem shared storage**: ZFS replication ou boot time failover (~2-5min)
**3. Fencing device configurado** - sem fencing = risco split-brain
---
## Workflow Completo
### Fase 1: Fencing Configuration
Detalhes completos das 3 opcoes (Watchdog, IPMI, Network) em: `references/fencing-configuration.md`
**Resumo:** Watchdog para inicio, IPMI para producao, evitar network fencing.
### Fase 2: HA Manager Configuration
```bash
# Verificar status
ha-manager status
# Expected: quorum: OK, master: <node-name> (elected), lrm: active
```
**Criar HA Groups por criticidade:**
```bash
# Critical (priority 100)
ha-manager groupadd critical \
--nodes "server.descomplicar.pt:100,cluster.descomplicar.pt:100"
# Medium (priority 50)
ha-manager groupadd medium \
--nodes "server.descomplicar.pt:50,cluster.descomplicar.pt:50"
# Low (priority 10)
ha-manager groupadd low \
--nodes "server.descomplicar.pt:10,cluster.descomplicar.pt:10"
```
### Fase 3: Adicionar VMs a HA
```bash
# VM 200 (EasyPanel Docker)
ha-manager add vm:200 \
--group critical \
--max_restart 3 \
--max_relocate 2 \
--state started
# VM 300 (CWP)
ha-manager add vm:300 \
--group critical \
--max_restart 3 \
--max_relocate 2 \
--state started
```
**Parametros:**
- `max_restart`: Tentativas restart no mesmo node antes de relocate
- `max_relocate`: Maximo relocates entre nodes
- `state started`: HA Manager garante VM esta sempre started
```bash
# Verificar
ha-manager status
```
### Fase 4: Failover Testing
Procedimentos detalhados de teste (shutdown clean, node crash simulado, live migration) e tuning de policies em: `references/failover-testing.md`
### Fase 5: Production Rollout
Abordagem faseada (low -> medium -> critical) com monitorizacao de 30 dias.
Documentar runbook em: `06-Operacoes/Procedimentos/D7-Tecnologia/PROC-HA-Failover.md`
---
## Best Practices
**Fazer:**
- Testar failover em VMs teste ANTES production
- Configurar fencing (watchdog minimo, IPMI ideal)
- Monitorizar quorum 24/7
- Documentar runbooks failover
- Backup ANTES activar HA
**Nao fazer:**
- HA sem fencing (risco split-brain)
- max_relocate muito alto (VM fica "bouncing")
- Assumir instant failover sem shared storage
- Testar failover em production sem plano
---
## Troubleshooting
### VM nao failover
```bash
ha-manager status | grep vm:ID
pvecm status
journalctl -u pve-ha-crm -f
```
### Split-brain detected
```bash
# Shutdown 1 node completamente
systemctl poweroff
# No node restante:
pvecm expected 1 # Force quorum com 1 node
# Resolver networking, rejoin node shutdown
```
### Failover loop (VM keeps restarting)
```bash
# Pause HA temporario
ha-manager set vm:ID --state disabled
# Fix VM issue
# Re-enable HA
ha-manager set vm:ID --state started
```
---
## References
- `references/fencing-configuration.md` - Detalhes Watchdog, IPMI e Network fencing
- `references/failover-testing.md` - Testes, policies, monitoring, alertas, production rollout
- **NotebookLM:** 276ccdde-6b95-42a3-ad96-4e64d64c8d52
- **HA Manager Docs:** https://pve.proxmox.com/pve-docs/ha-manager.1.html
- **Fencing:** https://pve.proxmox.com/wiki/Fencing
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,185 @@
# Failover Testing - Proxmox HA
Procedimentos detalhados de teste de failover e monitoring.
---
## Criar VM Teste HA
```bash
# VM 999 para teste (nao production)
qm create 999 --name ha-test --memory 512 --cores 1
# Adicionar a HA
ha-manager add vm:999 --state started
```
## Teste 1: Shutdown Clean
```bash
# Node onde VM 999 corre:
qm shutdown 999
# HA Manager deve:
# 1. Detectar shutdown (~30s)
# 2. Tentar restart no mesmo node (max_restart vezes)
# 3. Se continua down -> relocate para outro node
# Monitorizar
watch -n 1 'ha-manager status | grep vm:999'
```
## Teste 2: Node Crash (Simulado)
```bash
# CUIDADO: Apenas em teste, nao production
# Shutdown abrupto do node onde VM 999 corre
# (simula hardware failure)
echo b > /proc/sysrq-trigger # Reboot forcado
# Outro node deve:
# 1. Detectar node down via quorum (~1min)
# 2. Fence node (via watchdog/IPMI)
# 3. Boot VM 999 no node surviving
# Timeline esperado:
# - 0s: Node crash
# - ~60s: Quorum detecta node missing
# - ~90s: Fencing executado
# - ~120s: VM boota em outro node
# Total downtime: ~2-3min (sem shared storage)
# Com shared storage: ~30-60s
```
## Teste 3: Live Migration Manual
```bash
# Migration manual (com VM running)
qm migrate 999 <target-node-name> --online
# Com shared storage: <10s downtime
# Sem shared storage: copia disk = lento (GB/min)
# Para production VMs:
# - Fazer em janela manutencao se sem shared storage
# - Live migration OK se shared storage
```
---
## HA Policies e Tuning
### Shutdown Policy
```bash
# Default: conditional (HA Manager decide)
# Opcoes: conditional, freeze, failover, migrate
# Para VMs criticas que NAO devem migrar durante manutencao:
ha-manager set vm:200 --state freeze
# Para forcar migrate durante manutencao:
ha-manager set vm:200 --state migrate
```
### Maintenance Mode
```bash
# Colocar node em maintenance (nao recebe novos VMs HA)
ha-manager set-node-state <node-name> maintenance
# VMs HA existentes:
# - Nao migram automaticamente
# - Mas nao recebem novas em failover
# Sair de maintenance
ha-manager set-node-state <node-name> active
```
### Priorities (Load Balance)
```bash
# Preferencia de nodes por VM
# VM 200: Preferir Node B
ha-manager set vm:200 --group critical --restricted
# restricted: VM so corre nos nodes do grupo
# unrestricted: VM pode correr em qualquer node (fallback)
```
---
## Monitoring e Alertas
### HA Manager Logs
```bash
# Logs HA Manager
journalctl -u pve-ha-lrm -f # Local Resource Manager
journalctl -u pve-ha-crm -f # Cluster Resource Manager
# Ver decisoes de failover
grep "migrate\|relocate" /var/log/pve/tasks/index
```
### Configurar Alertas
```bash
# Via Web UI: Datacenter -> Notifications
# Email alerts para:
# - Node down
# - Quorum lost
# - VM failover events
# - Fencing executed
# SMTP: mail.descomplicar.pt
# To: admin@descomplicar.pt
```
### Script Monitorizacao (cron cada 5min)
```bash
#!/bin/bash
# /usr/local/bin/check-ha-health.sh
ha_status=$(ha-manager status | grep "quorum:" | awk '{print $2}')
if [ "$ha_status" != "OK" ]; then
echo "HA Quorum NOT OK" | mail -s "ALERT: HA Issue" admin@descomplicar.pt
fi
# Cron
# */5 * * * * /usr/local/bin/check-ha-health.sh
```
---
## Production Rollout
Phased approach:
```bash
# Week 1: VMs nao-criticas (teste)
ha-manager add vm:250 --group low
# Week 2: VMs medias (se Week 1 OK)
ha-manager add vm:201,202 --group medium
# Week 3: VMs criticas (se tudo OK)
ha-manager add vm:200,300 --group critical
```
### Documentar Runbook
Criar: `06-Operacoes/Procedimentos/D7-Tecnologia/PROC-HA-Failover.md`
Conteudo:
- Detectar failover event
- Validar VM booted correctamente
- Investigar causa node failure
- Restore node original
- Migrate VM back (se necessario)
@@ -0,0 +1,60 @@
# Fencing Configuration - Proxmox HA
Configuracao detalhada de fencing devices (STONITH) para High Availability.
---
## Opcao A: Watchdog (Software Fencing)
Mais simples, menos confiavel:
```bash
# Instalar watchdog em ambos nodes
apt install watchdog
# Load kernel module
modprobe softdog
# Auto-load on boot
echo "softdog" >> /etc/modules
# Configurar HA Manager para usar watchdog
# (automatico quando HA activado)
```
## Opcao B: IPMI/iLO (Hardware Fencing)
Mais confiavel, requer IPMI:
```bash
# Verificar IPMI disponivel
ipmitool lan print
# Configurar IPMI credentials (via BIOS ou ipmitool)
# Configurar em Proxmox (Web UI):
# Datacenter -> Fencing -> Add
# Type: IPMI
# IP: <node-ipmi-ip>
# Username: admin
# Password: <ipmi-pass>
# Test
fence_ipmilan -a <node-ipmi-ip> -l admin -p <pass> -o status
```
## Opcao C: Network Fencing (Menos Confiavel)
Usar apenas se IPMI nao disponivel:
```bash
# SSH-based fencing (perigoso)
# Depende de network estar up
# Nao recomendado para production
```
## Recomendacao Cluster Descomplicar
- **Inicio:** Watchdog (simple, funcional)
- **Producao:** IPMI se hardware suporta
- **Evitar:** Network fencing
@@ -0,0 +1,164 @@
---
name: proxmox-setup
description: Instalacao completa de Proxmox VE 8.x em Hetzner -- installimage, ZFS RAID-1, NAT networking single-IP e vSwitch com optimizacoes.
layer: wiki
---
# Proxmox Setup
Instalacao completa e configuracao de Proxmox VE 8.x em servidor dedicado Hetzner com ZFS RAID-1, networking NAT single-IP e optimizacoes.
## Quando Usar
- Instalar novo node Proxmox em servidor Hetzner
- Setup inicial com ZFS mirror NVMe
- Configurar networking NAT para single-IP
- Preparar node para clustering futuro
## Sintaxe
```bash
/proxmox-setup <server-ip> <hostname> [--zfs-pool rpool] [--arc-max 16G] [--vswitch]
```
## Knowledge Sources (Consultar SEMPRE)
### NotebookLM Proxmox Research
```bash
mcp__notebooklm__notebook_query \
notebook_id:"276ccdde-6b95-42a3-ad96-4e64d64c8d52" \
query:"proxmox installation hetzner zfs networking"
```
### Hub Docs
- `Hub/05-Projectos/Cluster Descomplicar/Research/Proxmox-VE/Guia-Definitivo-Proxmox-Hetzner.md`
---
## Workflow Completo
### Fase 1: Pre-Installation Checks
```bash
# Via SSH MCP - verificar Rescue Mode
mcp__ssh-unified__ssh_execute \
server:"hetzner-rescue" \
command:"uname -a && df -h"
```
Consultar NotebookLM para hardware specs e template correcto.
### Fase 2: installimage com ZFS RAID-1
Detalhes completos do template installimage e configuracao ZFS em: `references/installimage-zfs.md`
Resumo do workflow:
1. Instalar Debian 12 com LVM via installimage
2. Boot em Debian
3. Instalar ZFS + Proxmox
4. Criar ZFS pool para VMs (Opcao A recomendada: LVM root, ZFS para VMs)
### Fase 3: Proxmox VE 8.x Installation
```bash
# Adicionar repo Proxmox
echo "deb [arch=amd64] http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-install-repo.list
# Adicionar key
wget https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg -O /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
# Update e instalar
apt update && apt full-upgrade
apt install proxmox-ve postfix open-iscsi chrony
# Remover kernel Debian (apos boot em PVE kernel)
apt remove linux-image-amd64 'linux-image-6.1*'
update-grub
reboot
```
### Fase 4: ZFS Tuning
Detalhes em: `references/installimage-zfs.md` (seccao ZFS Tuning)
### Fase 5: Networking NAT (Single-IP Hetzner)
Configuracao completa de NAT, port forwarding e vSwitch em: `references/networking-nat.md`
### Fase 6: Proxmox Web UI + Storage
```
https://SERVER_IP:8006
User: root
Password: (root password do servidor)
```
```bash
# Comentar enterprise repo
sed -i 's/^deb/#deb/' /etc/apt/sources.list.d/pve-enterprise.list
apt update
```
Storage no Web UI: Datacenter -> Storage -> Add (Directory, ZFS, PBS).
### Fase 7: Validation Checklist
```bash
pveversion -v # PVE version
zpool status # ZFS status
zpool list && zfs list
ping -c 3 8.8.8.8 # Networking
curl -k https://localhost:8006 # Web UI
arc_summary | grep "ARC size" # ARC stats
```
**Security Hardening:**
```bash
sed -i 's/#PermitRootLogin yes/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl restart sshd
```
---
## Troubleshooting
| Problema | Comando diagnostico |
|----------|-------------------|
| Web UI nao acessivel | `systemctl status pveproxy` + `journalctl -u pveproxy -f` |
| VMs sem internet (NAT) | `cat /proc/sys/net/ipv4/ip_forward` + `iptables -t nat -L -n -v` |
| ZFS ARC nao limita | `cat /sys/module/zfs/parameters/zfs_arc_max` |
| vSwitch MTU issues | `ping -M do -s 1372 10.0.0.2` |
---
## Quando NAO Usar
- Para servidores non-Hetzner (diferentes gotchas de networking)
- Para Proxmox ja instalado (usar outras skills de config)
- Para troubleshooting (criar skill especifica)
---
## References
- `references/installimage-zfs.md` - Template installimage, conversao ZFS, tuning ARC
- `references/networking-nat.md` - NAT single-IP, port forwarding, vSwitch, gotchas Hetzner
- **NotebookLM:** 276ccdde-6b95-42a3-ad96-4e64d64c8d52 (150+ fontes)
- **Proxmox Docs:** https://pve.proxmox.com/pve-docs/pve-admin-guide.html
- **Hetzner Docs:** https://docs.hetzner.com/robot/dedicated-server/
---
Projecto: Cluster Proxmox Descomplicar (#65) | Tarefa: #1712
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,112 @@
# installimage e ZFS - Proxmox Hetzner
Detalhes de instalacao via installimage e configuracao ZFS.
---
## Template installimage
Template base para 2x NVMe 1TB + HDD 16TB:
```bash
DRIVE1 /dev/nvme0n1
DRIVE2 /dev/nvme1n1
SWRAID 0
SWRAIDLEVEL 0
BOOTLOADER grub
HOSTNAME HOSTNAME_PLACEHOLDER
PART /boot ext3 1024M
PART lvm vg0 all
LV vg0 root / ext4 50G
LV vg0 swap swap swap 16G
LV vg0 tmp /tmp ext4 10G
LV vg0 home /home ext4 20G
IMAGE /root/images/Debian-bookworm-latest-amd64-base.tar.gz
```
## Executar installimage
```bash
# No Rescue Mode
installimage
# Seleccionar Debian 12 (Bookworm)
# Copiar template acima
# Salvar e confirmar
# Reboot automatico
```
## Conversao para ZFS (Pos-Install)
**IMPORTANTE:** installimage nao suporta ZFS directamente. Workflow:
1. Instalar Debian 12 com LVM (installimage)
2. Boot em Debian
3. Instalar ZFS + Proxmox
4. Migrar para ZFS pool (ou aceitar LVM para root, ZFS para VMs)
### Opcao A: ZFS para VMs apenas (RECOMENDADO para Hetzner)
```bash
# Criar ZFS pool em NVMe para VMs
zpool create -f \
-o ashift=12 \
-o compression=lz4 \
-o atime=off \
rpool mirror /dev/nvme0n1p3 /dev/nvme1n1p3
# Criar datasets
zfs create rpool/vm-disks
zfs create rpool/ct-volumes
```
### Opcao B: ZFS root (AVANCADO - requer reinstall manual)
- Nao suportado por installimage
- Requer particionamento manual + debootstrap
- Consultar: https://pve.proxmox.com/wiki/ZFS_on_Linux
**Recomendacao para Cluster Descomplicar:** Opcao A (LVM root, ZFS para VMs)
---
## ZFS Tuning (128GB RAM)
### Configurar ARC Limits
```bash
# ARC max 16GB (deixa 110GB para VMs)
# ARC min 4GB
echo "options zfs zfs_arc_max=17179869184" >> /etc/modprobe.d/zfs.conf
echo "options zfs zfs_arc_min=4294967296" >> /etc/modprobe.d/zfs.conf
# Aplicar
update-initramfs -u -k all
```
### Optimizar ZFS para NVMe
```bash
# Verificar ashift (deve ser 12 para NVMe 4K sectors)
zdb -C rpool | grep ashift
# Activar compression LZ4 (se ainda nao)
zfs set compression=lz4 rpool
# Disable atime (performance)
zfs set atime=off rpool
# Snapshot visibility
zfs set snapdir=hidden rpool
```
### Criar ZFS Datasets para PBS (se HDD 16TB)
```bash
# Dataset para PBS datastore
zfs create rpool/pbs-datastore
zfs set mountpoint=/mnt/pbs-datastore rpool/pbs-datastore
zfs set compression=lz4 rpool/pbs-datastore
zfs set dedup=off rpool/pbs-datastore
```
@@ -0,0 +1,120 @@
# Networking NAT e vSwitch - Proxmox Hetzner
Configuracao de rede NAT single-IP e vSwitch para Proxmox em Hetzner.
---
## Networking NAT (Single-IP Hetzner)
### Configurar /etc/network/interfaces
Template para Single-IP NAT:
```bash
auto lo
iface lo inet loopback
# Interface fisica (verificar nome com 'ip a')
auto eno1
iface eno1 inet static
address SERVER_IP/32
gateway GATEWAY_IP
pointopoint GATEWAY_IP
# Bridge interna para VMs (NAT)
auto vmbr0
iface vmbr0 inet static
address 10.10.10.1/24
bridge-ports none
bridge-stp off
bridge-fd 0
# NAT masquerading
post-up echo 1 > /proc/sys/net/ipv4/ip_forward
post-up iptables -t nat -A POSTROUTING -s '10.10.10.0/24' -o eno1 -j MASQUERADE
post-down iptables -t nat -D POSTROUTING -s '10.10.10.0/24' -o eno1 -j MASQUERADE
```
**CRITICAL Hetzner Gotchas:**
- Gateway /32 point-to-point (nao /24 ou /26)
- IP e gateway podem estar em subnets diferentes
- Verificar IP real e gateway no Hetzner Robot
### Aplicar Networking
```bash
# Test config
ifup --no-act vmbr0
# Apply
systemctl restart networking
# Verificar
ip a
ping -c 3 8.8.8.8
```
### Port Forwarding (Opcional - para expor VMs)
```bash
# Exemplo: Redirecionar porta 8080 host -> porta 80 VM 10.10.10.100
iptables -t nat -A PREROUTING -i eno1 -p tcp --dport 8080 -j DNAT --to 10.10.10.100:80
# Persistir com iptables-persistent
apt install iptables-persistent
iptables-save > /etc/iptables/rules.v4
```
---
## vSwitch Configuration
### Configurar VLAN no Robot Panel
- Hetzner Robot -> vSwitch -> Create VLAN
- Anotar VLAN ID (ex: 4000)
### Adicionar ao /etc/network/interfaces
```bash
# vSwitch interface (MTU 1400 OBRIGATORIO)
auto enp7s0.4000
iface enp7s0.4000 inet manual
mtu 1400
# Bridge vSwitch
auto vmbr1
iface vmbr1 inet static
address 10.0.0.1/24
bridge-ports enp7s0.4000
bridge-stp off
bridge-fd 0
mtu 1400
```
**CRITICAL:** MTU 1400 nao negociavel para vSwitch Hetzner.
---
## Hetzner-Specific Gotchas (CRITICAL)
### 1. MAC Filtering
**Problema:** Bridged networking com MAC nao registado = bloqueado
**Solucao aplicada:** NAT masquerading (bypass MAC filtering)
**Alternativa:** Pedir virtual MAC no Robot panel (gratis)
### 2. Gateway Point-to-Point
**Problema:** Gateway fora da subnet do IP principal
**Solucao:** `address IP/32` + `pointopoint GATEWAY` (nao /24 ou /26)
### 3. vSwitch MTU 1400
**Problema:** vSwitch Hetzner requer MTU 1400 (nao 1500 standard)
**Solucao:** Forcar `mtu 1400` em vmbr1 e enp7s0.4000
### 4. ZFS vs LVM Trade-off
**Problema:** installimage nao suporta ZFS root directo
**Solucao:** LVM para root (compatibilidade), ZFS para VMs (performance)
### 5. Kernel PVE vs Debian
**Problema:** Kernel stock Debian nao optimizado para virtualizacao
**Solucao:** Instalar proxmox-ve + remover kernel Debian
@@ -0,0 +1,177 @@
---
name: security-audit
description: Auditoria de seguranca para aplicacoes web e servidores -- identificacao de vulnerabilidades, verificacao OWASP/GDPR e recomendacoes de hardening.
layer: wiki
---
# /security-audit - Security Compliance Specialist
Auditoria de seguranca seguindo padroes OWASP Top 10, GDPR e best practices Descomplicar.
---
## Quando usar
- Auditar codigo para vulnerabilidades
- Verificar configuracoes de seguranca servidor
- Avaliar compliance GDPR/ISO
- Security review de aplicacoes
- Analise de logs de seguranca
- Pentest basico (ethical hacking)
- Hardening de servidor/aplicacao
---
## Protocolo obrigatorio
### 1. Pesquisa inicial (SEMPRE)
```javascript
// Antes de qualquer audit, consultar historico
await mcp__mem_search_memories({
query: "security vulnerabilidade [sistema/projecto]",
limit: 5
});
```
### 2. Verificar contexto
- [ ] Vulnerabilidades anteriores no sistema
- [ ] Incidentes de seguranca passados
- [ ] Politicas de seguranca existentes
- [ ] Compliance requirements (GDPR, ISO, etc.)
- [ ] Stack tecnologico e versoes
---
## Workflow principal
### Passo 1: OWASP Top 10
Executar checklist completa por categoria. Ver detalhes e exemplos de codigo em:
**-> [references/owasp-checklist.md](references/owasp-checklist.md)**
Resumo das 10 categorias:
| # | Categoria | Foco principal |
|---|-----------|----------------|
| A01 | Broken Access Control | IDOR, RBAC, path traversal |
| A02 | Cryptographic Failures | bcrypt, HTTPS, cookies seguras |
| A03 | Injection | SQL, command, template injection |
| A04 | Insecure Design | Rate limiting, MFA, session timeout |
| A05 | Security Misconfiguration | Headers, directory listing, ficheiros expostos |
| A06 | Vulnerable Components | Dependencias desactualizadas, CVEs |
| A07 | Auth Failures | Session fixation, user enum, lockout |
| A08 | Data Integrity | Deserialization, SRI, CI/CD |
| A09 | Logging Failures | Logs de acesso, retencao, alertas |
| A10 | SSRF | Whitelist de dominios, validacao URL |
### Passo 2: Classificar findings
| Severidade | Score CVSS | Prazo correcao |
|------------|-----------|----------------|
| **Critico** | 9-10 | Imediato (<24h) |
| **Alto** | 7-8.9 | 24-48h |
| **Medio** | 4-6.9 | 7 dias |
| **Baixo** | 1-3.9 | 30 dias |
**Calculo CVSS v3:** https://www.first.org/cvss/calculator/3.1
### Passo 3: Gerar relatorio
Usar template completo com findings, compliance OWASP, GDPR e plano de remediacao:
**-> [references/report-template.md](references/report-template.md)**
O template inclui:
- Executive summary com score global
- Findings por severidade (com evidencia e remediacao)
- Tabela OWASP Top 10 compliance
- Checklist GDPR completo (consentimento, direitos, seguranca, documentacao)
- Ferramentas SAST/DAST utilizadas
- Hardening checklists (Linux, Web Server, PHP, MySQL)
### Passo 4: Hardening (se aplicavel)
Aplicar correcoes seguindo checklists em `references/report-template.md` (seccao Hardening).
---
## Ferramentas rapidas
```bash
# Headers HTTP
curl -I https://site.com | grep -E "X-|Content-Security|Strict-Transport"
# SSL Labs
curl "https://api.ssllabs.com/api/v3/analyze?host=site.com"
# Dependencias PHP
composer audit
# Dependencias Node
npm audit
# WordPress
wpscan --url https://site.com --api-token YOUR_TOKEN
```
---
## Contexto NotebookLM
Consultar ANTES de executar para contexto especializado:
| Notebook | ID | Consultar quando |
|----------|-----|-----------------|
| Ciberseguranca WordPress | 5f60adfd-2435-4725-8c12-9c11c5f51d75 | Sempre |
```
mcp__notebooklm__notebook_query({
notebook_id: "5f60adfd-2435-4725-8c12-9c11c5f51d75",
query: "<adaptar ao contexto de auditoria>"
})
```
---
## Anti-patterns
| Anti-pattern | Problema | Solucao |
|--------------|----------|---------|
| Auditar sem historico | Repetir trabalho | Consultar memoria primeiro |
| Ignorar severidade | Tudo parece urgente | Classificar com CVSS |
| Relatorio sem evidencia | Nao accionavel | Incluir PoC e remediacao |
| Expor credenciais | Fuga de dados | Mascarar PII no relatorio |
| Auditar parcialmente | Falsa seguranca | OWASP Top 10 completo |
---
## Confidencialidade
- **Nunca** expor credenciais em relatorios
- **Mascarar** dados sensiveis (`email: *****@domain.com`)
- **Restringir** relatorios a stakeholders autorizados
- **Eliminar** ficheiros temporarios apos analise
---
## Ficheiros de referencia
| Ficheiro | Conteudo |
|----------|----------|
| [references/owasp-checklist.md](references/owasp-checklist.md) | OWASP Top 10 detalhado com codigo vulneravel vs seguro e testes |
| [references/report-template.md](references/report-template.md) | Template relatorio, GDPR checklist, ferramentas SAST/DAST, hardening checklists |
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,275 @@
# /security-audit - OWASP Top 10 Checklist Detalhada
Verificações e exemplos de código para cada categoria OWASP Top 10.
## A01 - Broken Access Control
**Verificações:**
```php
// ❌ VULNERÁVEL
if ($_GET['user_id']) {
$user = getUserById($_GET['user_id']); // Sem verificação ownership
showUserData($user);
}
// ✅ SEGURO
if ($_GET['user_id']) {
$user = getUserById($_GET['user_id']);
if ($user->id !== $currentUser->id && !$currentUser->isAdmin()) {
die('Unauthorized');
}
showUserData($user);
}
```
**Testes:**
- [ ] Tentar aceder recurso de outro user (`/user/123` → `/user/124`)
- [ ] Testar IDOR (Insecure Direct Object Reference)
- [ ] Verificar se RBAC implementado correctamente
- [ ] Path traversal (`../../../etc/passwd`)
- [ ] Forced browsing (aceder `/admin` sem autenticação)
---
## A02 - Cryptographic Failures
**Verificações:**
```php
// ❌ VULNERÁVEL
$password = md5($_POST['password']); // MD5 é inseguro
// ✅ SEGURO
$password = password_hash($_POST['password'], PASSWORD_BCRYPT);
```
**Checklist:**
- [ ] Passwords com bcrypt/argon2 (não MD5/SHA1)
- [ ] Dados sensíveis encriptados em BD
- [ ] HTTPS em produção (não HTTP)
- [ ] Cookies com flags `Secure` e `HttpOnly`
- [ ] Sem PII em logs
**Testes:**
```bash
# Verificar HTTPS
curl -I https://site.com | grep -i "strict-transport-security"
# Verificar cookies
curl -I https://site.com | grep -i "set-cookie"
# Deve ter: Secure; HttpOnly; SameSite=Strict
```
---
## A03 - Injection (SQL, Command, LDAP)
**SQL Injection:**
```php
// ❌ VULNERÁVEL
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
// ✅ SEGURO (Prepared Statements)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
```
**Testes SQL:**
```sql
-- Payloads comuns
' OR '1'='1
'; DROP TABLE users; --
' UNION SELECT NULL, NULL, NULL--
```
**Command Injection:**
```php
// ❌ VULNERÁVEL
system("ping " . $_GET['host']);
// ✅ SEGURO
$host = escapeshellarg($_GET['host']);
system("ping $host");
```
**Checklist:**
- [ ] Prepared statements em todas as queries
- [ ] Input sanitizado antes de shell commands
- [ ] NoSQL injection prevenido
- [ ] LDAP injection prevenido
- [ ] Template injection prevenido (Twig, Smarty)
---
## A04 - Insecure Design
**Checklist:**
- [ ] Rate limiting implementado (login, API)
- [ ] CAPTCHA em formulários públicos
- [ ] MFA disponível para contas admin
- [ ] Session timeout configurado
- [ ] Password policy forte (min 8 chars, uppercase, etc.)
**Testes:**
```bash
# Testar rate limiting
for i in {1..100}; do
curl -X POST https://site.com/login \
-d "email=test@test.com&password=wrong" &
done
# Deve bloquear após N tentativas
```
---
## A05 - Security Misconfiguration
**Checklist Servidor:**
```bash
# Verificar versões expostas
curl -I https://site.com | grep -i "server:"
# Não deve revelar Apache/2.4.41 ou PHP/8.1.2
# Verificar directory listing
curl https://site.com/uploads/
# Verificar ficheiros sensíveis expostos
curl https://site.com/.env
curl https://site.com/.git/config
curl https://site.com/phpinfo.php
# Todos devem dar 403/404
```
**Headers de Segurança:**
```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server_tokens off;
```
---
## A06 - Vulnerable and Outdated Components
**Verificações:**
```bash
# PHP - Composer
composer audit
# Node.js
npm audit
# WordPress
wp core version
wp plugin list --format=json | jq '.[] | select(.update=="available")'
```
**Checklist:**
- [ ] Dependências actualizadas (últimos 3 meses)
- [ ] Sem bibliotecas com vulnerabilidades conhecidas
- [ ] PHP/Node/Python versão suportada
- [ ] WordPress core e plugins actualizados
---
## A07 - Identification and Authentication Failures
**Verificações:**
```php
// ❌ VULNERÁVEL - Session Fixation
session_start();
// ✅ SEGURO
session_start();
session_regenerate_id(true); // Regenerar após login
```
**Checklist:**
- [ ] Session regenerate após login
- [ ] Logout invalida sessão no servidor
- [ ] Session timeout (15-30 min)
- [ ] Password reset com token expirável
- [ ] Account lockout após N tentativas falhadas
- [ ] Sem user enum (login não revela se user existe)
**Testes:**
```bash
# Testar user enumeration
curl -X POST https://site.com/login \
-d "email=naoexiste@test.com&password=wrong"
# Não deve dizer "user não existe"
```
---
## A08 - Software and Data Integrity Failures
**Verificações:**
```php
// ❌ VULNERÁVEL
$data = unserialize($_GET['data']);
// ✅ SEGURO
$data = json_decode($_GET['data'], true);
```
**SRI Example:**
```html
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
crossorigin="anonymous"></script>
```
---
## A09 - Security Logging and Monitoring Failures
**Checklist:**
- [ ] Login attempts logged (success/fail)
- [ ] Access to sensitive data logged
- [ ] Logs não contêm passwords ou PII
- [ ] Alertas configurados para actividade suspeita
- [ ] Retenção de logs (mínimo 90 dias)
**Análise de Logs:**
```bash
# Tentativas de SQL injection
grep -i "union.*select" /var/log/nginx/access.log
# Tentativas de path traversal
grep -E "\.\./|\.\.%2F" /var/log/nginx/access.log
# Scans de vulnerabilidades
grep -E "nikto|nmap|sqlmap" /var/log/nginx/access.log
# Tentativas de login falhadas
grep "Failed password" /var/log/secure
```
---
## A10 - Server-Side Request Forgery (SSRF)
**Verificações:**
```php
// ❌ VULNERÁVEL
$url = $_GET['url'];
file_get_contents($url); // Pode aceder rede interna
// ✅ SEGURO
$url = $_GET['url'];
$parsed = parse_url($url);
if ($parsed['host'] !== 'allowed-domain.com') {
die('Invalid URL');
}
// Whitelist de domínios permitidos
```
**Testes:**
```bash
curl "https://site.com/fetch?url=http://localhost/admin"
curl "https://site.com/fetch?url=http://192.168.1.1"
```
@@ -0,0 +1,237 @@
# /security-audit - Template de Relatório e Ferramentas
## Template Relatório de Segurança
```markdown
# RELATORIO DE SEGURANCA
**Sistema:** [Nome do Sistema/Aplicação]
**Data:** [YYYY-MM-DD]
**Auditor:** Security Compliance Specialist - Descomplicar®
**Scope:** [Frontend | Backend | Infra | Full Stack]
---
## EXECUTIVE SUMMARY
[2-3 parágrafos resumindo estado de segurança global]
**Score Global:** [X]/100
**CVSS Médio:** [X.X]
**Breakdown:**
- Críticos: [N] findings
- Altos: [N] findings
- Médios: [N] findings
- Baixos: [N] findings
---
## FINDINGS
### CRITICOS ([N] findings)
#### FINDING-001: SQL Injection em Login
- **Severidade:** CRÍTICO (CVSS 9.8)
- **Componente:** `login.php` linha 45
- **Categoria OWASP:** A03 - Injection
**Descrição:**
Parâmetro `username` não sanitizado, permitindo SQL injection.
**Evidência:**
```php
$sql = "SELECT * FROM users WHERE username = '" . $_POST['username'] . "'";
```
**Proof of Concept:**
```bash
curl -X POST https://site.com/login \
-d "username=' OR '1'='1&password=anything"
```
**Impacto:**
- Acesso total à base de dados
- Bypass de autenticação
- Exfiltração de dados de clientes
**Remediação:**
```php
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$_POST['username']]);
```
**Prazo:** IMEDIATO
**Status:** OPEN
---
## OWASP TOP 10 COMPLIANCE
| # | Categoria | Status | Findings |
|---|-----------|--------|----------|
| A01 | Broken Access Control | Parcial | 2 médios |
| A02 | Cryptographic Failures | OK | 0 |
| A03 | Injection | Falha | 1 crítico, 1 alto |
| A04 | Insecure Design | Parcial | 1 médio |
| A05 | Security Misconfiguration | Atenção | 3 altos |
| A06 | Vulnerable Components | Parcial | 5 baixos |
| A07 | Auth Failures | OK | 0 |
| A08 | Data Integrity | OK | 0 |
| A09 | Logging Failures | Parcial | 1 médio |
| A10 | SSRF | OK | 0 |
---
## COMPLIANCE GDPR
- [ ] Consentimento explícito capturado
- [ ] Direito ao esquecimento implementado
- [ ] Portabilidade de dados possível
- [ ] Retenção de dados definida
- [ ] Encriptação de dados sensíveis
- [ ] Logging de acessos activo
**Score GDPR:** 75/100 (Parcialmente Conforme)
---
**Confidencial - Descomplicar® Crescimento Digital**
```
---
## Checklist GDPR Completo
### Consentimento
- [ ] Opt-in explícito (não pré-seleccionado)
- [ ] Linguagem clara
- [ ] Granular (newsletter ≠ tracking)
- [ ] Fácil de retirar
### Direitos do Titular
- [ ] Direito ao acesso (exportar dados)
- [ ] Direito à rectificação (editar dados)
- [ ] Direito ao esquecimento (delete account)
- [ ] Direito à portabilidade (formato standard)
- [ ] Direito à oposição (opt-out processing)
### Segurança
- [ ] Encriptação em trânsito (HTTPS)
- [ ] Encriptação em repouso (BD sensível)
- [ ] Passwords com hash forte (bcrypt)
- [ ] Logs de acesso a dados pessoais
- [ ] Notificação de breach (<72h)
### Documentação
- [ ] Privacy policy actualizada (última 12 meses)
- [ ] Cookie policy clara
- [ ] Terms of service
- [ ] Data processing agreement (DPA)
---
## Ferramentas de Análise
### Análise Estática (SAST)
```bash
# PHP - PHPStan
phpstan analyse --level=max src/
# PHP - Psalm
psalm --show-info=true
# PHP - Security Checker
local-php-security-checker
# JavaScript - ESLint
eslint --ext .js,.jsx src/
# Python - Bandit
bandit -r project/
```
### Análise Dinâmica (DAST)
```bash
# OWASP ZAP (headless)
zap-cli quick-scan -s all https://site.com
# Nikto
nikto -h https://site.com
# SQLMap
sqlmap -u "https://site.com/page?id=1" --batch
# Nmap
nmap -sV -sC site.com
# WPScan (WordPress)
wpscan --url https://site.com --api-token YOUR_TOKEN
```
---
## Hardening Checklist
### Sistema Operativo (Linux)
- [ ] Firewall activo (iptables/firewalld)
- [ ] SELinux/AppArmor activado
- [ ] SSH com key-based auth (não password)
- [ ] SSH não permite root login
- [ ] Fail2ban instalado e configurado
- [ ] Automatic security updates
- [ ] Portas desnecessárias fechadas
### Web Server (Nginx/Apache)
- [ ] Server tokens desactivado
- [ ] Directory listing desactivado
- [ ] Security headers configurados
- [ ] SSL/TLS configurado (A+ SSL Labs)
- [ ] Apenas TLS 1.2+ (não SSL, TLS 1.0, 1.1)
- [ ] HTTP/2 activado
### PHP
- [ ] `display_errors = Off` em produção
- [ ] `expose_php = Off`
- [ ] `open_basedir` configurado
- [ ] `disable_functions` configurado
- [ ] File uploads limitados
- [ ] OPcache activado
### MySQL
- [ ] Bind apenas localhost (se possível)
- [ ] Root sem acesso remoto
- [ ] Passwords fortes
- [ ] `skip-name-resolve`
- [ ] Slow query log activado
---
## Severidade de Findings
```
CRÍTICO (Score 9-10)
Exploração imediata possível, dados em risco
RCE, SQL Injection, Authentication Bypass
→ Corrigir IMEDIATAMENTE (<24h)
ALTO (Score 7-8.9)
Vulnerabilidade significativa
XSS Stored, CSRF, Insecure Deserialization
→ Corrigir em 24-48h
MÉDIO (Score 4-6.9)
Risco moderado
XSS Reflected, Information Disclosure, Missing Headers
→ Corrigir em 7 dias
BAIXO (Score 1-3.9)
Melhoria recomendada
Weak Password Policy, Verbose Errors, Outdated Libraries
→ Corrigir em 30 dias
```
**Cálculo CVSS v3:** https://www.first.org/cvss/calculator/3.1
@@ -0,0 +1,161 @@
---
name: security-check
description: Auditoria de seguranca de infraestrutura — certificados SSL, portas abertas, actualizacoes pendentes, backups recentes, CSF/firewall e email security. Score por servidor.
context: fork
layer: wiki
---
# /security-check v1.0 (infraestrutura)
Auditoria de seguranca dos servidores Descomplicar. Verifica SSL, portas, actualizacoes, backups, firewall e email security.
> **Auditoria de dependencias de codigo:** usar `/dep-audit` (plugin dev-tools).
**Referencia:** PROC-Security-Audit-API-Keys.md | PROC-Backup-Sistema.md | Memory: `infra.md`, `cwp-ssl-autorenew-fix.md`
---
## Servidores Alvo
| Alias | Host | IP | SO | Firewall | Gestor SSL |
|-------|------|----|----|----------|------------|
| server | server.descomplicar.pt | 176.9.3.158 | CentOS (CWP) | CSF v15 | acme.sh |
| easy | qibspu.easypanel.host | 178.63.18.51 | Ubuntu 24.04 | — | EasyPanel/Traefik |
| gateway | gateway.descomplicar.pt | — | — | — | nginx/acme |
---
## Modos de Execucao
| Comando | Descricao |
|---------|-----------|
| `/security-check` | Auditoria completa (todos os servidores, todos os checks) |
| `/security-check server` | Apenas servidor CWP |
| `/security-check easy` | Apenas EasyPanel |
| `/security-check ssl` | Apenas certificados SSL (todos os servidores) |
| `/security-check quick` | Resumo rapido: SSL a expirar + backups + firewall |
---
## Protocolo de Execucao
### Check 1: Certificados SSL (server)
```bash
# Executar via mcp__ssh-unified__ssh_execute(server="server")
# Listar todos os certs e datas de expiracao
for dir in /root/.acme.sh/cwp_certs/*_ecc; do
domain=$(basename "$dir" | sed 's/_ecc$//')
cert="$dir/${domain}.cer"
if [ -f "$cert" ]; then
expiry=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2)
days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
if [ "$days" -lt 15 ]; then
echo "CRITICAL $domain expires in ${days}d ($expiry)"
elif [ "$days" -lt 30 ]; then
echo "WARN $domain expires in ${days}d ($expiry)"
else
echo "OK $domain expires in ${days}d"
fi
fi
done | sort
```
**Thresholds:**
- **OK:** >30 dias para expirar
- **WARN:** 15-30 dias
- **CRITICAL:** <15 dias
### Check 2: Portas Abertas (server)
```bash
# Portas a escutar no servidor CWP
ss -tlnp | awk 'NR>1 {print $4}' | sed 's/.*://' | sort -n | uniq
# Portas esperadas (CWP padrao):
# 22(SSH) 25(SMTP) 80(HTTP) 110(POP3) 143(IMAP) 443(HTTPS)
# 465(SMTPS) 587(Submission) 993(IMAPS) 995(POP3S)
# 2031(CWP) 2087(CWP Admin) 3306(MySQL)
# 8443(CWP SSL) 9443(SSH alternativo)
EXPECTED="22 25 80 110 143 443 465 587 993 995 2031 2087 3306 8443 9443"
# Comparar: portas abertas que NAO estao na lista esperada
ss -tlnp | awk 'NR>1 {print $4}' | sed 's/.*://' | sort -n | uniq | while read port; do
if ! echo "$EXPECTED" | grep -qw "$port"; then
proc=$(ss -tlnp | grep ":${port} " | sed 's/.*users:(("//' | cut -d'"' -f1)
echo "UNEXPECTED :${port} ($proc)"
fi
done
```
### Check 3: Actualizacoes Pendentes
```bash
# CWP Server (CentOS/dnf)
# mcp__ssh-unified__ssh_execute(server="server")
dnf check-update --quiet 2>/dev/null | grep -v "^$" | wc -l
# Se >0: listar security updates
dnf updateinfo list security 2>/dev/null | tail -10
# EasyPanel (Ubuntu/apt)
# mcp__ssh-unified__ssh_execute(server="easy")
apt list --upgradable 2>/dev/null | grep -c upgradable
# Se >0: listar security updates
apt list --upgradable 2>/dev/null | grep -i secur
```
**Thresholds:**
- **OK:** 0 security updates pendentes
- **WARN:** 1-5 security updates
- **CRITICAL:** >5 security updates OU kernel update pendente
### Check 4: Backups Recentes (server)
```bash
# mcp__ssh-unified__ssh_execute(server="server")
echo "=== MySQL Daily ==="
ls -lt /home/backup/mysql/daily/ 2>/dev/null | head -3
latest_mysql=$(find /home/backup/mysql/daily/ -name "*.sql.gz" -mtime -2 2>/dev/null | wc -l)
echo "BDs com backup <48h: $latest_mysql"
echo "=== Monthly ==="
ls -lt /home/backup/monthly/ 2>/dev/null | head -5
echo "=== Rsync Easy->Server ==="
# Verificar ultimo rsync (Easy replica para Server as 06:00)
ls -lt /gordito/backup-easy/ 2>/dev/null | head -3 || echo "path nao existe"
echo "=== Idade ultimo backup ==="
newest=$(find /home/backup/ -type f -name "*.gz" -printf '%T@\n' 2>/dev/null | sort -rn | head -1)
if [ -n "$newest" ]; then
age_hours=$(echo "scale=1; ($(date +%s) - $newest) / 3600" | bc)
echo "Backup mais recente: ${age_hours}h atras"
fi
```
**Thresholds:**
- **OK:** backup MySQL <48h, monthly <35 dias
- **WARN:** backup MySQL 48-72h, monthly 35-60 dias
- **CRITICAL:** backup MySQL >72h, monthly >60 dias
### Check 5: Firewall CSF (server)
```bash
# mcp__ssh-unified__ssh_execute(server="server")
echo "=== CSF Status ==="
csf -v
csf -l | head -5
echo "=== IPs Bloqueados ==="
wc -l /etc/csf/csf.deny
echo "Ultimos 5 bloqueios:"
tail -5 /etc/csf/csf.deny
echo "=== LFD Status ==="
systemctl is-active lfd
echo "=== Ataques Recentes (24h) ==="
grep "$(date
@@ -0,0 +1,176 @@
---
name: server-health
description: Diagnostico completo de servidor -- monitorizacao de recursos, estado de servicos e identificacao de problemas de performance.
layer: wiki
---
# /server-health - Diagnostico Completo de Servidor
Checklist sistematico para diagnosticar problemas de performance ou auditar estado do servidor CWP.
---
## Contexto NotebookLM
Consultar ANTES de executar para contexto especializado:
| Notebook | ID | Consultar quando |
|----------|-----|-----------------|
| Cloud e Infraestrutura TI | f9a79b5a-649f-4443-afaf-7ff562b6c2e7 | Sempre |
```
mcp__notebooklm__notebook_query({
notebook_id: "f9a79b5a-649f-4443-afaf-7ff562b6c2e7",
query: "<adaptar ao contexto do pedido>"
})
```
### Procedimentos relacionados
- [PROC-Backup-Sistema.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/Seguranca/PROC-Backup-Sistema.md)
---
## Servidor alvo
| Servidor | IP | MCP | User |
|----------|-----|-----|------|
| **CWP Principal** | 176.9.3.158 | ssh-unified | root |
```javascript
mcp__ssh-unified__ssh_execute({
server: "server",
command: "<comando>"
})
```
---
## Modos de execucao
| Comando | Descricao |
|---------|-----------|
| `/server-health` | Diagnostico completo (todos os checks) |
| `/server-health quick` | Apenas metricas criticas (load, RAM, disco) |
| `/server-health mysql` | Focus em MySQL e bases de dados |
| `/server-health ssl` | Verificacao de todos os certificados SSL |
| `/server-health sites` | Status de todos os sites WordPress |
---
## Workflow completo
Executar os 7 passos em sequencia. Comandos detalhados para cada passo em:
**-> [references/commands.md](references/commands.md)**
### Passo 1: Sistema (paralelo)
Recolher: load average, RAM, swap, disco, top processos CPU.
### Passo 2: MySQL
Recolher: status geral, processos activos, threads, slow queries, tamanho BDs, tabelas corrompidas.
### Passo 3: Web Servers
Verificar: Apache, Nginx, conexoes activas, GZIP, virtual hosts.
### Passo 4: PHP
Verificar: versao, memory limit, OPcache, PHP-FPM workers.
### Passo 5: SSL/Certificados
Listar certificados, verificar renovacoes, testar dominios especificos.
### Passo 6: Seguranca
Verificar: Fail2ban, portas abertas, logins SSH, firewall.
### Passo 7: Sites WordPress
Verificar todos os sites: versao WP, tamanho wp-content, acesso HTTP.
---
## Thresholds de alertas
| Metrica | OK | Warning | Critical |
|---------|-----|---------|----------|
| **Load (1m)** | <2 | 2-5 | >5 |
| **RAM** | <70% | 70-85% | >85% |
| **Swap** | <10% | 10-30% | >30% |
| **Disco** | <70% | 70-85% | >85% |
| **MySQL Threads** | <50 | 50-100 | >100 |
| **Slow Queries** | 0 | 1-10 | >10 |
| **Sites WP OK** | 100% | 90-99% | <90% |
| **SSL Expiring** | >30d | 15-30d | <15d |
---
## Template de output
Ver template completo em `references/commands.md` (seccao Template de Output).
Inclui:
- Resumo executivo com tabela de metricas e health score
- Alertas criticos e warnings
- Detalhes por seccao (sistema, MySQL, web, PHP, SSL, seguranca, sites WP)
- Recomendacoes priorizadas (urgente, importante, melhorias)
---
## Instrucoes de implementacao
1. Executar comandos em paralelo quando possivel (mcp__ssh-unified__ssh_execute)
2. Calcular health score: (checks OK / total checks) x 100
3. Gerar alertas baseados em thresholds
4. Agrupar recomendacoes por urgencia
5. Incluir troubleshooting especifico para cada problema detectado
---
## Anti-patterns
| Anti-pattern | Problema | Solucao |
|--------------|----------|---------|
| Assumir SSH como root | Pode falhar | Verificar permissoes primeiro |
| Executar comandos sequenciais | Lento | Paralelo quando possivel |
| Ignorar exit codes | Falhas silenciosas | Validar cada comando |
| Nao limpar recursos temp | Consume espaco | Limpeza em cada audit |
| Nao documentar problemas | Sem historico | Gravar em Desk/Obsidian |
---
## Checklist de execucao
- [ ] SSH conecta (testar `whoami`)
- [ ] Todas as metricas recolhidas (sistema, mysql, web, ssl)
- [ ] Thresholds aplicados (calcular status)
- [ ] Health score calculado
- [ ] Alertas gerados (critico/warning)
- [ ] Recomendacoes priorizadas
- [ ] Troubleshooting incluido
- [ ] Report formatado (Markdown)
- [ ] Opcional: gravar em Desk ou Obsidian
---
## Ficheiros de referencia
| Ficheiro | Conteudo |
|----------|----------|
| [references/commands.md](references/commands.md) | Comandos SSH por passo, queries MySQL, quick mode, deep dive, limpeza cache, template output, troubleshooting |
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,232 @@
# /server-health - Comandos SSH e Template de Output
## Comandos por Passo
### Passo 1: Sistema
```bash
uptime
free -h
df -h
top -bn1 | head -20
cat /proc/loadavg
vmstat 1 3
ps aux | grep -E 'httpd|mysql|nginx' | grep -v grep
```
**Validação:**
- Load 1min <2: OK | 2-5: Warning | >5: Critical
- RAM <70%: OK | Swap >10%: Investigar
### Passo 2: MySQL
```bash
mysqladmin status
mysqladmin processlist | head -20
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads%';"
mysql -e "SHOW GLOBAL STATUS LIKE 'Slow_queries';"
mysql -e "SELECT table_schema, ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)' FROM information_schema.TABLES GROUP BY table_schema ORDER BY SUM(data_length + index_length) DESC LIMIT 10;"
mysqlcheck --all-databases --check
```
**Queries Críticas:**
```sql
-- Ver queries lentas em execução
SELECT id, user, host, db, command, time, state, LEFT(info, 100) as query
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC;
-- BDs maiores que 500MB
SELECT table_schema, ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.TABLES
GROUP BY table_schema
HAVING size_mb > 500
ORDER BY size_mb DESC;
```
### Passo 3: Web Servers
```bash
systemctl status httpd --no-pager | head -15
systemctl status nginx --no-pager | head -15
curl -sI localhost | head -10
curl -sI -H 'Accept-Encoding: gzip' localhost | grep -i encoding
netstat -an | grep :80 | wc -l
netstat -an | grep :443 | wc -l
httpd -S 2>&1 | grep -E 'port|namevhost' | head -20
```
### Passo 4: PHP
```bash
php -v | head -1
php -i | grep memory_limit
php -i | grep -E 'opcache.enable|opcache.memory_consumption'
ps aux | grep php-fpm | wc -l
```
### Passo 5: SSL/Certificados
```bash
ls -lh /root/.acme.sh/cwp_certs/
for d in $(ls /root/.acme.sh/cwp_certs/); do
echo "=== $d ==="
grep -E 'Le_NextRenewTimeStr|Le_Alt' /root/.acme.sh/cwp_certs/$d/*.conf 2>/dev/null | head -3
done
echo | openssl s_client -connect DOMINIO:443 2>/dev/null | openssl x509 -noout -dates -subject
tail -100 /root/.acme.sh/acme.sh.log | grep -E 'Renew|error|success'
```
**Validação SSL:**
- Renovação >30 dias: OK | 15-30 dias: Avisar | <15 dias: Urgente
### Passo 6: Segurança
```bash
systemctl status fail2ban --no-pager 2>/dev/null
fail2ban-client status 2>/dev/null | head -10
fail2ban-client status sshd 2>/dev/null | grep "Banned IP"
netstat -tlnp | grep LISTEN | head -20
last -10
grep "Failed password" /var/log/secure | tail -20
systemctl status firewalld --no-pager || iptables -L -n | head -20
```
### Passo 7: Sites WordPress
```bash
for dir in /home/*/public_html; do
if [ -f "$dir/wp-config.php" ]; then
domain=$(basename $(dirname $dir))
echo "=== $domain ==="
grep "wp_version = " $dir/wp-includes/version.php | head -1
du -sh $dir/wp-content
curl -sI "http://$domain" | head -3
fi
done
```
---
## Verificação Rápida (Quick Mode)
```bash
echo "=== QUICK HEALTH ===" && \
uptime && \
free -h | grep Mem && \
df -h | grep -E '^/dev/vda|Filesystem' && \
mysqladmin status 2>&1 | head -1 && \
systemctl is-active httpd nginx mysql
```
---
## MySQL Deep Dive
```bash
# Optimizar todas as BDs
mysqlcheck --optimize --all-databases
# Limpar logs binários antigos (>7 dias)
mysql -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);"
# Verificar fragmentação
mysql -e "SELECT table_schema, table_name,
ROUND(data_length/1024/1024,2) as data_mb,
ROUND(data_free/1024/1024,2) as free_mb
FROM information_schema.TABLES
WHERE data_free > 0
ORDER BY data_free DESC LIMIT 10;"
```
---
## Limpeza de Cache/Temp
```bash
# Limpar cache WordPress
find /home/*/public_html/wp-content/cache -type f -delete
# Limpar sessions PHP antigas
find /var/lib/php/session -type f -mtime +7 -delete
# Limpar logs Apache/Nginx >30 dias
find /home/*/logs -name "*.log" -mtime +30 -delete
```
---
## Template de Output
```markdown
# Server Health Report - YYYY-MM-DD HH:MM
## Resumo Executivo
| Métrica | Valor | Status | Limite |
|---------|-------|--------|--------|
| Load (1m) | X.XX | OK/Warn/Crit | <2 |
| RAM Usada | XX% | OK/Warn/Crit | <70% |
| Swap Usado | XX% | OK/Warn/Crit | <10% |
| Disco / | XX% | OK/Warn/Crit | <70% |
| MySQL Threads | XX | OK/Warn/Crit | <50 |
| Sites WP OK | XX/YY | OK/Warn/Crit | 100% |
| SSL Expiring | XX | OK/Warn/Crit | 0 |
**Health Score:** XX/100
---
## Alertas Críticos
> CRÍTICO: Load >5 há 10 minutos
> ATENÇÃO: Certificado SSL solarfv360.pt expira em 12 dias
---
## Recomendações Prioritárias
### URGENTE (Hoje)
1. Reduzir uso de Swap (45%)
2. Renovar SSL solarfv360.pt
### IMPORTANTE (Esta Semana)
3. Optimizar queries lentas MySQL
### MELHORIAS (Mês)
4. Configurar alertas automáticos
```
---
## Troubleshooting
### Swap Alto (>30%)
```bash
ps aux --sort=-%mem | head -10
swapoff -a && swapon -a
```
### Load Alto (>5)
```bash
ps aux --sort=-%cpu | head -10
iostat -x 1 5
top -bn1 | head -20
```
### MySQL Lento
```bash
mysqladmin processlist
tail -50 /var/log/mysql/slow.log
mysqlcheck --optimize --all-databases
```
### Sites WordPress com Erro
```bash
tail -100 /home/USER/logs/error.log
ls -lh /home/USER/public_html/wp-content
chown -R USER:USER /home/USER/public_html/wp-content
```
+170
View File
@@ -0,0 +1,170 @@
---
name: vm-migration
description: Migracao zero-downtime de workloads CWP e EasyPanel para VMs Proxmox com safety nets e rollback procedures.
layer: wiki
---
# VM Migration
Migracao zero-downtime de workloads CWP e EasyPanel para Proxmox VMs seguindo Migration-Plan-OptionA com safety nets e rollback procedures.
## Quando Usar
- Migrar servidores CWP para VMs Proxmox
- Migrar containers EasyPanel para VMs Proxmox
- Executar Migration-Plan-OptionA (3 fases)
- Migracao phased com validation periods
## Quando Nao Usar
- Migracoes non-CWP/EasyPanel (criar plan especifico)
- Ambientes teste/dev (menos rigor)
- Single-server setups (nao cluster)
## Sintaxe
```bash
/vm-migration <source-type> <source-host> [--phase 1|2|3] [--batch-size 5] [--validate-days 7]
```
## Exemplos
```bash
# Fase 1: EasyPanel migration (batch 5 containers)
/vm-migration easypanel easy.descomplicar.pt --phase 1 --batch-size 5
# Fase 2: CWP migration com 7 dias validation
/vm-migration cwp server.descomplicar.pt --phase 2 --validate-days 7
# Fase 3: Apenas confirmar (sem migracao)
/vm-migration finalize --phase 3
```
---
## Knowledge Sources (Consultar SEMPRE)
```bash
mcp__notebooklm__notebook_query \
notebook_id:"276ccdde-6b95-42a3-ad96-4e64d64c8d52" \
query:"proxmox migration cwp easypanel docker lxc zero downtime"
```
Hub Docs: `Hub/05-Projectos/Cluster Descomplicar/Planning/Migration-Plan-OptionA.md`
---
## Migration-Plan-OptionA Overview
**Timeline:** 8 semanas | **Strategy:** Phased migration com safety nets | **Rollback:** Disponivel em cada fase
```
Week 1-2: FASE 1 - EasyPanel Migration
+-- Backup EasyPanel -> PBS
+-- Create Docker VM Proxmox
+-- Migrate containers (batch 5-10)
+-- DNS cutover gradual
+-- Validation + Rollback window
Week 3-6: FASE 2 - CWP Migration
+-- 7 dias safety net (server intacto)
+-- Create AlmaLinux 8 VM
+-- Migrate CWP accounts (batch)
+-- Validate sites + email
+-- DNS cutover (TTL 300s)
+-- Rollback ate Day 7
Week 7-8: FASE 3 - Cluster Formation
+-- Prepare server.descomplicar.pt as Node A
+-- Form cluster (pvecm)
+-- Configure HA groups
+-- Live migration tests
+-- Cleanup legacy servers
```
---
## Workflow Pre-Migration (Todas Fases)
### 1. Backup Strategy Validation
```bash
# Verificar PBS configurado
pvesm status | grep pbs
# Criar backup point actual
vzdump --storage pbs-main --all 1 --mode snapshot
```
Garantir 3-2-1 compliance: original + PBS Node B + PBS Node A remote sync.
### 2. Documentar Estado Actual
```bash
# CWP: Listar contas
/scripts/list_accounts > /tmp/cwp-accounts.txt
# EasyPanel: Listar services
curl -s http://localhost:3000/api/trpc/projects.list | jq > /tmp/easypanel-services.json
# DNS TTLs (baixar para 300s ANTES de migration)
```
### 3. Comunicar Clientes (se downtime esperado)
- Email 48h antes
- Status page update
- Janela de manutencao agendada
---
## Fases Detalhadas
| Fase | Detalhes | Referencia |
|------|----------|------------|
| Fase 1: EasyPanel (Week 1-2) | 108 containers Docker -> VM Proxmox | [references/fase1-easypanel.md](references/fase1-easypanel.md) |
| Fase 2: CWP (Week 3-6) | 39 vhosts CWP -> VM AlmaLinux 8 | [references/fase2-cwp.md](references/fase2-cwp.md) |
| Fase 3: Cluster (Week 7-8) | 2-node cluster, HA, cleanup | [references/fase3-cluster.md](references/fase3-cluster.md) |
| Troubleshooting | Problemas comuns e resolucao | [references/troubleshooting.md](references/troubleshooting.md) |
---
## Backup Strategy Resumo
| Fase | Locais Backup | RPO | RTO |
|------|---------------|-----|-----|
| Fase 1 | Original + PBS + VPS | 1h | 2-4h |
| Fase 2 | Server intacto + PBS + /mnt/migration/ | 1h | 2-4h |
| Fase 3 | Node A + Node B + PBS dual | 1h | 2-4h |
---
## Anti-Patterns
| Anti-Pattern | Risco | Alternativa |
|--------------|-------|-------------|
| Migrar tudo de uma vez | Downtime total | Batches de 5-10 |
| Nao baixar TTL antes | DNS lento a propagar | TTL 300s 24h antes |
| Apagar server antigo cedo | Sem rollback | Manter 7 dias |
| Sem backup pre-migration | Perda dados | vzdump + 3-2-1 |
---
## References
- **Migration Plan:** Hub/05-Projectos/Cluster Descomplicar/Planning/Migration-Plan-OptionA.md
- **NotebookLM:** 276ccdde-6b95-42a3-ad96-4e64d64c8d52
- **Guia Hub:** Guia-Definitivo-Proxmox-Hetzner.md (Modulo 4: Workloads)
- **Desk CRM:** Projecto #65, Tarefa #1712
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,118 @@
# Fase 1: EasyPanel Migration (Week 1-2)
Target: Migrar 108 containers Docker para VM Proxmox
## 1.1 Criar VM Docker Host
```bash
qm create 200 \
--name easypanel-docker \
--memory 32768 \
--cores 8 \
--net0 virtio,bridge=vmbr0 \
--scsi0 rpool/vm-disks:200 \
--ostype l26 \
--boot order=scsi0
# Install Ubuntu 24.04 LTS
# Via Cloud-Init ou ISO manual
```
## 1.2 Instalar Docker + EasyPanel
```bash
ssh root@10.10.10.200
# Docker
curl -fsSL https://get.docker.com | sh
# EasyPanel
curl -sSL https://get.easypanel.io | sh
```
## 1.3 Backup Containers Actuais
```bash
# Em easy.descomplicar.pt
tar -czf /tmp/easypanel-volumes.tar.gz /var/lib/easypanel/projects
# Transfer para PBS ou storage temporario
scp /tmp/easypanel-volumes.tar.gz root@cluster.descomplicar.pt:/mnt/migration/
```
## 1.4 Migrar Containers (Batch 5-10)
Batch 1 (nao-criticos para teste):
```bash
# Containers teste: dev environments, staging
# IDs: 1-5
# Por cada container:
1. Exportar env vars do EasyPanel
2. Exportar docker-compose.yml
3. Copiar volumes
4. Recriar em novo EasyPanel
5. Testar health endpoint
6. DNS cutover se OK
```
Workflow Batch:
```bash
for container_id in 1 2 3 4 5; do
# Export config
curl -s http://easy.descomplicar.pt:3000/api/trpc/services.get \
-d "serviceId=$container_id" > config_$container_id.json
# Copiar volumes
rsync -avz /var/lib/easypanel/projects/$container_id/ \
root@10.10.10.200:/var/lib/easypanel/projects/$container_id/
# Recriar service (via EasyPanel API ou UI)
# Test
curl -I http://10.10.10.200:PORT/health
# DNS cutover (se health OK)
done
```
## 1.5 Validation (24-48h por batch)
```
Monitoring:
- Uptime checks (UptimeRobot ou similar)
- Error rates (logs)
- Performance (response time <500ms)
- Cliente feedback
Rollback triggers:
- >2 containers falham consecutivamente
- Cliente reporta down
- Health checks fail >10min
```
## 1.6 DNS Cutover
```bash
# Baixar TTL para 300s (5min) 24h ANTES
# Ex: Cloudflare ou dns.descomplicar.pt
# Cutover:
A record: old-ip -> NAT port forward para 10.10.10.200:PORT
# Monitorizar por 1h
# Reverter se problemas
```
## 1.7 Rollback Procedure
```bash
# Reverter DNS (TTL 300s = 5min propagacao)
# Reactivar container antigo
# Investigar causa falha
# Re-tentar apos fix
```
Batch 2-N: Repetir ate 108 containers migrados.
@@ -0,0 +1,112 @@
# Fase 2: CWP Migration (Week 3-6)
Target: Migrar 39 vhosts CWP para VM AlmaLinux 8
**CRITICAL:** 7 dias safety net - server.descomplicar.pt intacto
## 2.1 Criar VM AlmaLinux 8 + CWP
```bash
qm create 300 \
--name cwp-legacy \
--memory 16384 \
--cores 6 \
--net0 virtio,bridge=vmbr0 \
--scsi0 rpool/vm-disks:150 \
--ostype l26
# Instalar AlmaLinux 8
# Instalar CWP7
wget http://centos-webpanel.com/cwp-el8-latest
sh cwp-el8-latest
```
## 2.2 Backup CWP Accounts
```bash
# Em server.descomplicar.pt
for account in $(cat /tmp/cwp-accounts.txt); do
/scripts/pkgacct $account
done
# Transfer backups
rsync -avz /home/backup-*/cpmove-*.tar.gz \
root@cluster.descomplicar.pt:/mnt/migration/cwp/
```
## 2.3 Migrar Contas (Batch 3-5 contas)
Workflow por conta:
```bash
# 1. Restore backup em VM CWP
scp /mnt/migration/cwp/cpmove-ACCOUNT.tar.gz root@10.10.10.300:/home/
# 2. Restore via CWP
/scripts/restorepkg ACCOUNT
# 3. Validar:
- Site carrega (HTTP 200)
- Database conecta
- Email funciona (send test)
- SSL certificado valido
# 4. DNS cutover (TTL 300s)
A record: site.com -> 10.10.10.300 (via NAT port forward)
# 5. Monitorizar 24h
```
## 2.4 Validation Period (7 dias)
```bash
# Days 1-7 apos migration:
- Server antigo (server.descomplicar.pt) INTACTO
- Rollback instantaneo se problema critical
- Cliente pode reverter DNS manualmente se necessario
# Day 7: Confirmar com Emanuel
# Se tudo OK -> proceder cleanup
# Se problemas -> extend validation ou rollback completo
```
## 2.5 Email Migration
```bash
# Por cada conta CWP:
# 1. Backup mailboxes
tar -czf /tmp/mail-ACCOUNT.tar.gz /home/ACCOUNT/mail/
# 2. Transfer
scp /tmp/mail-ACCOUNT.tar.gz root@10.10.10.300:/tmp/
# 3. Restore
cd /home/ACCOUNT/
tar -xzf /tmp/mail-ACCOUNT.tar.gz
# 4. Fix permissions
chown -R ACCOUNT:ACCOUNT /home/ACCOUNT/mail/
# 5. Testar IMAP/SMTP
telnet localhost 143 # IMAP
telnet localhost 25 # SMTP
```
## 2.6 Rollback Procedure (Fase 2)
```bash
# Disponivel ate Day 7
# 1. Reverter DNS (todos sites)
A records -> IP antigo (server.descomplicar.pt)
# 2. Verificar server antigo online
ping server.descomplicar.pt
# 3. Comunicar clientes
# 4. Analisar causa falha
# 5. Ajustar plan e re-tentar
```
@@ -0,0 +1,63 @@
# Fase 3: Cluster Formation (Week 7-8)
Target: Formar cluster 2-node, HA, cleanup
## 3.1 Preparar server.descomplicar.pt como Node A
```bash
# APENAS apos Fase 2 100% validada
# Backup final completo
tar -czf /tmp/final-backup-server.tar.gz /etc /home /var/www
# Reformatar com Proxmox (/proxmox-setup)
# Tornar Node A do cluster
```
## 3.2 Cluster Formation
```bash
/proxmox-cluster create --node-a server.descomplicar.pt --node-b cluster.descomplicar.pt
```
## 3.3 HA Configuration
```bash
/proxmox-ha configure --critical-vms 200,300
```
## 3.4 Cleanup
```bash
# Cancelar easy.descomplicar.pt VPS (apos validacao)
# Backup final de tudo
# Documentar nova arquitectura
```
---
## Backup Strategy Durante Migration
### Fase 1 (EasyPanel)
3 locais:
1. Containers em easy.descomplicar.pt (original)
2. PBS Node B backup
3. easy.descomplicar.pt VPS backup (mantido durante Fase 1)
### Fase 2 (CWP)
Safety net 7 dias:
1. Server antigo intacto (rollback rapido)
2. VM CWP -> PBS backups automaticos
3. Backups manuais /mnt/migration/
RPO: 1h (PBS backups hourly se critical)
RTO: 2-4h (restore + DNS propagation)
### Fase 3 (Cluster)
Redundancia completa:
1. VMs em Node A + Node B
2. PBS primary (Node B 16TB)
3. PBS secondary remote sync (Node A 12TB)
@@ -0,0 +1,48 @@
# VM Migration - Troubleshooting
## Container migration fails
```bash
# Verificar logs
docker logs CONTAINER_ID
# Verificar volumes
ls -lah /var/lib/easypanel/projects/PROJECT/
# Testar manual
docker-compose up -d
# Rollback e investigar
```
## CWP site nao carrega apos migration
```bash
# Verificar Apache
systemctl status httpd
# Verificar vhost
cat /usr/local/apache/conf.d/vhosts/DOMAIN.conf
# Verificar database
mysql -u USER -p DATABASE
# Verificar DNS propagation
dig +short DOMAIN @8.8.8.8
```
## Email nao funciona
```bash
# Verificar Postfix
systemctl status postfix
# Testar SMTP
telnet localhost 25
# Verificar DNS MX
dig +short MX DOMAIN
# Verificar logs
tail -f /var/log/maillog
```