Skip to content
On this page

Plane

Project management tool. URL: plane.gunei.xyz — v1.2.2.

Admin panel: plane.gunei.xyz/god-mode

Las intake forms nativas de Plane requieren plan pago. Usamos Tally.so (free) + webhook como workaround.

Deployment

Docker Compose en /opt/apps/plane/. Red: gunei-network.

Servicios:

ServicioDescripción
webFrontend principal
spaceSpaces (portal público)
adminGod mode / admin panel
liveColaboración en tiempo real
apiAPI REST
workerWorker de tareas async
beat-workerScheduler (Celery beat)
migratorMigraciones de DB
plane-dbPostgreSQL
plane-redisRedis
plane-mqRabbitMQ
plane-minioMinIO (storage de archivos)
bash
cd /opt/apps/plane

# Levantar
docker compose up -d

# Bajar
docker compose down

# Ver logs
docker compose logs -f api
docker compose logs -f worker

Configuración

Variables clave en plane.env:

ini
# Dominio
WEB_URL=https://plane.gunei.xyz
CORS_ALLOWED_ORIGINS=https://plane.gunei.xyz

# Base de datos
PGHOST=plane-db
PGDATABASE=plane
PGUSER=plane
PGPASSWORD=<secret>

# Redis
REDIS_URL=redis://plane-redis:6379/

# RabbitMQ
RABBITMQ_HOST=plane-mq
RABBITMQ_DEFAULT_USER=plane
RABBITMQ_DEFAULT_PASS=<secret>
RABBITMQ_DEFAULT_VHOST=plane

# MinIO
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
AWS_ACCESS_KEY_ID=<secret>
AWS_SECRET_ACCESS_KEY=<secret>

# Email (via Postal)
EMAIL_HOST=postal.gunei.xyz
EMAIL_HOST_USER=<user>
EMAIL_HOST_PASSWORD=<secret>
EMAIL_FROM=noreply@gunei.xyz

Proxy Caddy

Bloque en el Caddyfile para plane.gunei.xyz:

nginx
plane.gunei.xyz {
    @spaces path /spaces*
    handle @spaces {
        reverse_proxy space:3000
    }

    @god-mode path /god-mode*
    handle @god-mode {
        reverse_proxy admin:3000
    }

    @live path /live*
    handle @live {
        reverse_proxy live:8090
    }

    @api path /api*
    handle @api {
        reverse_proxy api:8000
    }

    @auth path /auth*
    handle @auth {
        reverse_proxy api:8000
    }

    @uploads path /uploads*
    handle @uploads {
        reverse_proxy api:8000
    }

    handle {
        reverse_proxy web:3000
    }
}

Networking gotcha: Caddy corre en Docker (caddy-shared container). Para proxear a servicios corriendo en el host (no en Docker), usar host.docker.internal en vez de localhost. Requiere extra_hosts: ["host.docker.internal:host-gateway"] en el Docker Compose de Caddy.

Backups

PostgreSQL backup diario via cron — 3am, retención 7 días.

bash
# Ubicación de backups
ls /opt/apps/plane/backups/

# Verificar cron
crontab -l | grep plane

# Restore manual (ejemplo)
cat /opt/apps/plane/backups/plane_backup_YYYY-MM-DD.sql | docker exec -i plane-db psql -U plane -d plane

Integración Tally → Plane

Webhook server que recibe submissions de Tally y crea intake issues en Plane.

  • Endpoint: https://hooks.gunei.xyz/tally
  • Código: /root/webhook/server.py
  • Proxy: Caddy en caddy-sharedwebhook:8091

Cómo funciona

  1. Tally envía POST al webhook con la submission
  2. Server verifica firma HMAC-SHA256 con TALLY_SIGNING_SECRET
  3. Parsea campos del formulario (título, descripción, adjuntos)
  4. Crea intake issue via API de Plane (/intake-issues/ con payload {"issue": {...}})
  5. Sube adjuntos al issue (flujo de 3 pasos: presigned creds → POST multipart a Minio → PATCH mark complete)

Tally gotcha: Los file upload fields devuelven arrays de objetos con id, name, url, mimeType, size. Las URLs requieren header User-Agent para descargar.

Variables de entorno

ini
PLANE_API_KEY=<token de Plane>
PLANE_BASE_URL=https://plane.gunei.xyz
WORKSPACE_SLUG=gunei
PROJECT_ID=<uuid del proyecto>
TALLY_SIGNING_SECRET=<secret configurado en Tally>
PORT=8091

Correr el servidor

El servidor corre como contenedor Docker en gunei-network:

bash
# Ver estado
docker ps | grep webhook

# Restart
cd /root/webhook && docker compose restart

# Logs
docker logs -f webhook
bash
cd /root/webhook
docker compose up -d

Bugs conocidos

  • Minio EntityTooLarge: El POST multipart a Minio falla porque el body excede el Content-Length declarado del archivo. Fix propuesto: pedir size * 2 al solicitar las presigned credentials. Pendiente de implementar.

Agregar un nuevo formulario de intake

El server actual está hardcodeado a un solo proyecto y un solo formulario de Tally. Para agregar otro form (ej: otro proyecto de Plane, otro tipo de reporte), hay que tocar 3 cosas: Tally, el server, y Plane.

1. Crear el formulario en Tally

Ir a tally.so y crear un nuevo form. Tener en cuenta:

  • Los campos se mapean por label (el texto que el usuario ve). El server usa get_field(fields, "Label exacto") para extraer valores, así que los labels importan.
  • Si necesitás adjuntos, agregar un campo de tipo File Upload.
  • Una vez publicado, ir a Settings → Webhooks y configurar:
    • URL: https://hooks.gunei.xyz/<tu-nuevo-path> (ej: /tally-soporte)
    • Signing secret: generar uno nuevo con openssl rand -hex 32 y guardarlo.

2. Modificar el server

Editar /root/webhook/server.py:

a) Agregar constantes para el nuevo proyecto (al inicio del archivo):

python
# Proyecto Soporte (ejemplo)
PROJECT_ID_SOPORTE = "uuid-del-proyecto-en-plane"
TALLY_SIGNING_SECRET_SOPORTE = "nuevo-secret-generado"

b) Crear un handler para el nuevo form:

Copiar handle_tally() y adaptarlo. Lo que cambia:

  • El PROJECT_ID usado en las llamadas a plane_request
  • Los labels de get_field() — deben coincidir exactamente con los del nuevo form de Tally
  • La lógica de armado del titulo y description_html
python
def handle_soporte(payload):
    fields = payload["data"]["fields"]
    # Adaptar estos labels a los del nuevo form
    asunto = get_field(fields, "Asunto")
    detalle = get_field(fields, "Detalle")
    files = get_files(fields)

    titulo = asunto[:80] + ("..." if len(asunto) > 80 else "")
    description_html = f"<p>{detalle}</p>"

    result = plane_request("POST",
        f"/workspaces/{WORKSPACE_SLUG}/projects/{PROJECT_ID_SOPORTE}/intake-issues/",
        {"issue": {"name": titulo, "description_html": description_html, "priority": "none"}}
    )
    issue_id = result["issue"]
    print(f"Issue soporte creado: {issue_id}")

    for f in files:
        try:
            upload_attachment(issue_id, f, PROJECT_ID_SOPORTE)
        except Exception as e:
            print(f"  Error subiendo {f['name']}: {e}")

Ojo con upload_attachment(): esa función usa el PROJECT_ID global hardcodeado. Si el nuevo form es para otro proyecto, vas a tener que pasarle el project_id como parámetro en vez de usar la constante global.

c) Agregar la nueva ruta en el Handler:

python
def do_POST(self):
    if self.path == "/tally":
        secret = TALLY_SIGNING_SECRET
        handler = handle_tally
    elif self.path == "/tally-soporte":
        secret = TALLY_SIGNING_SECRET_SOPORTE
        handler = handle_soporte
    else:
        self.send_response(404)
        self.end_headers()
        return

    # verificar firma con el secret correspondiente
    # ... (misma lógica de HMAC, usando `secret`)
    handler(payload)

d) Reiniciar el server:

bash
cd /root/webhook && docker compose restart

3. Habilitar intake en el proyecto de Plane

El proyecto destino tiene que tener intake habilitado, si no la API devuelve 403.

  1. Ir a plane.gunei.xyz → Proyecto → Settings → Features
  2. Activar Intake (puede aparecer como "Triage" en algunas versiones)

4. Obtener el PROJECT_ID

Ir al proyecto en Plane → Settings → General. El UUID aparece en la URL del browser:

https://plane.gunei.xyz/gunei/projects/UUID-ACA/settings/

Checklist rápido

  • [ ] Form creado en Tally con los campos necesarios
  • [ ] Webhook configurado en Tally apuntando a hooks.gunei.xyz/<path>
  • [ ] Signing secret generado y configurado en ambos lados
  • [ ] Handler creado en server.py con los labels correctos
  • [ ] Ruta agregada en do_POST
  • [ ] upload_attachment recibe project_id si es otro proyecto
  • [ ] Intake habilitado en el proyecto de Plane
  • [ ] Server reiniciado
  • [ ] Probar con un submit de prueba y verificar con docker logs -f webhook

Operaciones comunes

bash
# Restart completo
docker compose restart

# Restart servicio específico
docker compose restart api worker

# Update a nueva versión
# 1. Editar imagen en docker-compose.yml
# 2. Pull
docker compose pull
# 3. Recrear con migración
docker compose up -d migrator
docker compose logs -f migrator  # esperar que termine
docker compose up -d

Troubleshooting

bash
# Verificar que todos los containers estén healthy
docker compose ps

# Minio: verificar acceso
docker exec plane-minio mc ls local/uploads/

# Worker: verificar que procesa tareas
docker compose logs --tail=50 worker

# API: health check
curl -s https://plane.gunei.xyz/api/health/ | jq

# Redis: verificar conexión
docker exec plane-redis redis-cli ping

# RabbitMQ: verificar colas
docker exec plane-mq rabbitmqctl list_queues