Pipeline Instagram · End-to-end¶
Última revisão: 2026-04-29
Costura render do squad (workflows/junior-instagram-post.md) com publicação automática (INSTAGRAM.md) num único caminho operacional.
┌──────────────────────────────────────────────────────────────────────┐
│ SQUAD (junior-squad) │
│ Helena → Cláudia → Caio → Yara/Rafa → Bia → studio/outputs/*.png │
└──────────────────────────────┬───────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ scripts/publish-junior-carousel.sh │
│ 1. lê PNGs em outputs/junior-maia/em-producao/<slug>/ │
│ 2. extrai caption de copy.md (versão curta) ou --caption-file │
│ 3. sobe pro Supabase Storage (instagram-media/<account>/YYYY-MM/) │
│ 4. POST /instagram/posts com scheduled_at │
│ 5. salva PUBLISH-LOG.md na pasta da peça │
└──────────────────────────────┬───────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ Supabase Postgres · instagram_posts(status='scheduled') │
└──────────────────────────────┬───────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ Cron `instagram-publisher` (apiclickup, */1 * * * *) │
│ Lê posts onde scheduled_at <= now(), publica via Graph API, │
│ marca published + ig_media_id ou failed + erro │
└──────────────────────────────┬───────────────────────────────────────┘
↓
📱 Instagram (no ar)
Quando usar¶
| Cenário | Caminho |
|---|---|
| Peça nova rodada pelo squad (squad termina em PNGs) | scripts/publish-junior-carousel.sh |
| Quer agendar manualmente sem render do squad (já tem URLs) | curl direto pra /instagram/posts (ver INSTAGRAM.md) |
| Publicação editorial via Sheets (calendário) | server/scripts/instagram-from-drive.js |
| Pausar publicações sem perder agendamentos | Admin → Crons → toggle instagram-publisher |
Caminho rápido (script)¶
Pré-requisitos (uma vez):
- server/.env com SUPABASE_URL, SUPABASE_SERVICE_KEY, DASHBOARD_TOKEN
- Bucket instagram-media público no Supabase (já criado)
- jq e curl instalados (brew install jq se faltar)
- Pasta da peça em outputs/junior-maia/em-producao/<slug>/ com:
- PNGs nomeados *slide<N>.png ou *card<N>.png (ordem alfanumérica determina ordem do carrossel)
- copy.md com a seção ### Versão curta (caption será extraída) ou caption.md na pasta
Comando¶
./scripts/publish-junior-carousel.sh \
--slug tudo-e-codigo \
--scheduled-at 2026-04-29T12:00:00Z
Flags úteis¶
| Flag | Default | Quando usar |
|---|---|---|
--account <junior\|gita> |
junior |
Publicar na conta da Gita |
--source <pasta> |
outputs/junior-maia/em-producao/<slug>/ |
Peça em outra pasta |
--caption-file <path> |
extrai de copy.md |
Caption customizada |
--type <feed\|reel\|carousel\|story> |
auto (>1 mídia = carousel) | Forçar tipo |
--server <url> |
https://gita-apiclickup.ewzc9p.easypanel.host |
Apontar pra outro server |
--dry-run |
— | Imprime payload sem subir nada |
--yes / -y |
— | Pula confirmação interativa |
Validações que o script faz¶
scheduled_atem ISO 8601 UTC comZ- Conta válida (
juniorougita) - Limites Meta: carousel 2-10 itens, feed/story 1, caption ≤ 2200 chars
- Bucket público (smoke test: a 1ª URL responde HTTP 200 antes de criar o post)
- Resposta do servidor traz
id(caso contrário, falha explícita)
Saída¶
- Post criado no Supabase com
status=scheduled - Cron
instagram-publisherpega no próximo tick (≤ 1 min) sescheduled_at <= now(). Se for futuro, espera. - Log salvo em
outputs/junior-maia/em-producao/<slug>/PUBLISH-LOG.mdcom: - Post ID
- URLs públicas das mídias (auditáveis)
- Comandos prontos pra checar status, forçar tick, debug
Caminho manual (sem script)¶
Use quando quiser controle granular ou quando o script não cobre o caso (ex: misturar imagens já hospedadas em outro lugar).
# 1. Carregar env
set -a && . server/.env && set +a
# 2. Subir cada mídia (repetir pra cada slide)
TS=$(date +%s)
for i in 1 2 3 4 5 6 7; do
KEY="instagram-media/junior/$(date -u +%Y-%m)/tudo-e-codigo-$TS-slide$i.png"
curl -sS -X POST "$SUPABASE_URL/storage/v1/object/$KEY" \
-H "Authorization: Bearer $SUPABASE_SERVICE_KEY" \
-H "Content-Type: image/png" \
-H "x-upsert: true" \
--data-binary "@studio/outputs/2026-04-29_tudo-e-codigo-slide$i.png"
done
# 3. Criar post
curl -X POST "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/posts" \
-H "Authorization: Bearer $DASHBOARD_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"account": "junior",
"post_type": "carousel",
"caption": "...",
"media_items": [
{"url": "<URL_pública_slide1>", "type": "image"},
...
],
"scheduled_at": "2026-04-29T12:00:00Z",
"status": "scheduled"
}'
Estrutura completa do payload em INSTAGRAM.md § API REST.
Monitoring¶
Status global do scheduler¶
curl -sS "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/scheduler/status" \
-H "Authorization: Bearer $DASHBOARD_TOKEN"
Retorna {cronRegistered, enabled, pendingCount, batchLimit, cronExpression}. Se enabled=false → ENABLE_INSTAGRAM_PUBLISHER=false no Easypanel ou cron pausado pelo admin.
Detalhes de um post específico¶
curl -sS "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/posts?status=scheduled&account=junior" \
-H "Authorization: Bearer $DASHBOARD_TOKEN" \
| jq '.posts[] | select(.id=="<POST_ID>")'
Forçar tick agora (debug)¶
curl -sS -X POST "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/publish-pending?limit=5" \
-H "Authorization: Bearer $DASHBOARD_TOKEN"
Útil pra: posts vencidos que ficaram presos, validar que o cron consome corretamente, debug de credenciais Graph API.
Admin UI¶
https://gita-apiclickup.ewzc9p.easypanel.host/instagram/admin?token=<DASHBOARD_TOKEN>
Preview visual tipo Instagram. Read-only — não cria/edita pelo admin.
Recuperação de falhas¶
| Status | O que aconteceu | O que fazer |
|---|---|---|
scheduled mas pendingCount=0 no scheduler depois do horário |
Cron não rodou (server fora do ar?) | Checar /health. Se voltar, próximo tick consome. |
failed + error: code=190 |
Token Meta expirou | Regerar Page Token (passo 5 em INSTAGRAM.md) → atualizar META_IG_TOKEN_* no Easypanel → redeploy. |
failed + error: code=4 |
Rate limit (>25 posts em 24h) | Esperar. Cron tenta de novo no próximo tick. |
failed + error: code=9004 |
URL de mídia inacessível | Bucket Supabase não está público. Tornar público e fazer PATCH no post (status volta pra scheduled). |
failed + status_code=ERROR no container |
Meta rejeitou (formato/tamanho) | Ver limites em INSTAGRAM.md § Limites da Meta. Re-render com formato correto + re-upload + criar post novo (descartar antigo). |
publishing há >5min |
Travou no fluxo de 2 etapas | Pode ser timeout do polling. Checar Easypanel logs do apiclickup. |
Re-tentar manualmente¶
Pelo admin: Instagram → card vermelho → Tentar novamente. Pelo banco: UPDATE instagram_posts SET status='scheduled', error=NULL WHERE id='<uuid>' (Supabase SQL editor).
Limpeza pós-publicação¶
Quando status=published, o post fica registrado em instagram_posts com ig_media_id. Não deletar — é audit trail. As mídias no bucket podem ser limpas depois de N dias se o storage encher (>10GB).
Tomás (arquivista) faz post-mortem em data/brand/junior-maia/08-aprendizados/posts/<DATA>_<slug>.md puxando métricas e o ig_media_id.
Troubleshooting do script¶
| Sintoma | Causa provável | Fix |
|---|---|---|
mapfile: command not found |
bash do macOS é antigo (3.2) | Já tratado — script usa while read |
jq: invalid JSON text |
Caption tem caractere especial não escapado | Salvar caption em arquivo + --caption-file |
Smoke test failed (HTTP 400) |
Bucket não está público | Console Supabase → Storage → instagram-media → toggle Public |
Servidor não retornou id |
Token DASHBOARD inválido OU server fora | curl /health no server primeiro |
| Imagens fora de ordem no Instagram | Naming não-natural (slide1, slide11, slide2) | Renomear pra slide01, slide02, ... ou card01, card02, ... |
Refs¶
- Workflow do squad: workflows/junior-instagram-post.md
- Pipeline técnica de publicação: docs/automations/INSTAGRAM.md
- Cron registry: docs/automations/CRONS.md
- Script: scripts/publish-junior-carousel.sh
- Server: server/automations/instagram-scheduler.js