Pular para o conteúdo
Development

O rastreador de syndication que não conseguia responder sua própria pergunta

Por Victor Da Luz
railsrubyinfrastructuredev-logblog-manager

Construí o blog-manager por um motivo principal: eu faço cross-post dos artigos do vdaluz.com para Medium e Dev.to, e queria um único lugar que soubesse o que já estava publicado onde. Essa semana sentei para de fato usá-lo para isso e percebi que não conseguia. Os dados estavam todos lá no banco. A UI simplesmente se recusava a me contar.

O problema da ausência invisível

O índice de posts tinha uma coluna de syndication com pequenas pills de status: Medium, Dev.to, Social. Minha decisão de design original era renderizar uma pill só quando algo tivesse acontecido naquele destino, então um post que nunca foi syndicated ficava “quieto” com um traço apagado. Parecia limpo na época.

Na prática, isso inverteu o sinal. Minha pergunta real é “quais posts ainda NÃO estão no Medium?” e a resposta estava codificada como a ausência de uma pill, o que você não consegue escanear em 86 linhas. Pior, uma linha mostrando só uma pill de Social era ambígua: será que o Medium está faltando, pendente, ou nunca foi tentado? O estado exato vivia na cor da pill mais um tooltip ao passar o mouse. Usuários daltônicos, usuários de touch, e eu às 23h perdemos todos igualmente.

A correção foi vergonhosamente pequena. Sempre renderizar as três pills, e adicionar um estado explícito de “não publicado” com um ponto vazado:

STATES = {
  done:    { css: "status-set",     label: "done" },
  pending: { css: "status-soon",    label: "in progress" },
  failed:  { css: "status-expired", label: "failed" },
  none:    { css: "status-none",    label: "not published" }
}.freeze

Mais filtros. O índice já tinha um padrão ?filter=missing_hero, então adicionei needs_medium, needs_devto e failed. Uma ressalva: o estado por destino vive em uma coluna JSON serializada (postiz_publish), então esses filtros rodam em Ruby com @posts.select(&:needs_medium?) em vez de SQL. Com 86 posts sem paginação essa é a troca certa; deixei um comentário dizendo isso para o meu eu futuro, que inevitavelmente vai tentar “consertar” isso.

Becos sem saída no fluxo de publicação

Publicar um artigo no Medium passa pelo Postiz como rascunho, depois um humano (eu) clica em publicar na UI do Postiz. O app mostrava rascunhos staged como texto apagado dizendo “Awaiting publish in Postiz”, com a explicação em um tooltip. Ele dizia onde estava o próximo passo e não dava nenhuma forma de chegar lá. Agora é um link para o calendário de lançamentos do Postiz, derivado da URL base da API que já está nas configurações:

def postiz_web_url
  postiz_base_url.sub(%r{/api/public/v\d+/?\z}, "")
end

Mesmo tratamento para pré-requisitos não atendidos. A publicação no Dev.to precisa de uma URL ativa, uma imagem de destaque e um token do GitHub, e o botão de Publish desabilitado explicava isso só ao passar o mouse. Agora ele diz o que está faltando diretamente: “needs a header image and a GitHub token”.

Também estendi o compartilhamento para que os blurbs sociais possam apontar para a cópia do Medium ou do Dev.to em vez de só a URL canônica. A tubulação já estava conectada (share_url(source) existia mas ignorava seu argumento), então a feature inteira foi fazer shareable_urls retornar os release_urls por provedor quando eles existem. O seletor de UI apareceu sozinho; ele já estava codificado para aparecer quando há mais de uma fonte.

O que a revisão pegou

A passada de revisão valeu a pena dessa vez. O achado que importou: um teste de sistema ainda verificava o texto antigo “Awaiting publish in Postiz”. Nunca percebi porque bin/rails test não roda testes de sistema, e o job de teste de sistema no CI é continue-on-error. Teria sido mergeado silenciosamente vermelho e continuado quebrado na main. A revisão também me empurrou a fazer os guards do controller chamarem o mesmo predicado needs_medium? que os filtros usam, em vez de manter uma cópia manual da condição que acabaria divergindo.

Aí o build falhou, duas vezes, e não era meu código

Todas as checagens de código passaram e o job de build do Docker morreu resolvendo docker.io/docker/dockerfile:1. Essa é a imagem do frontend do BuildKit puxada pela diretiva # syntax= na linha 1 do Dockerfile gerado pelo Rails. Eu já tinha movido a imagem base para um mirror de registry porque o Docker Hub limita a taxa do IP de saída do homelab, mas a diretiva de syntax era uma segunda dependência do Docker Hub, mais traiçoeira. Apaguei ela; o frontend embutido cobre tudo que um Dockerfile comum usa.

A nova execução então deu timeout contra o mirror também, o que significava que nunca tinha sido rate limiting. Os containers do runner self-hosted não tinham nenhuma conectividade de saída. Host bem, containers mortos, regras de NAT presentes, política FORWARD ACCEPT. A pista estava nos contadores: pacotes entravam na chain DOCKER-USER e nunca saíam. Uma mudança paralela no homelab tinha adicionado regras de firewall ali para restringir o acesso de entrada às portas de proxy do app, combinando só pela porta de destino. A DOCKER-USER vê tráfego encaminhado nas duas direções, então essas regras também derrubavam toda conexão de saída dos containers para as portas 80 e 443: pulls de registry e, de forma desconfortável, as chamadas de produção para Postiz, GitHub e Pexels. As regras tinham sido salvas com netfilter-persistent e ficaram dormentes até os dois containers reiniciarem naquela noite. A correção foi limitar o escopo delas com -i eth0 para que combinassem só com tráfego vindo da LAN.

Duas lições que vou guardar. Primeiro, “o CI está vermelho” e “seu diff está errado” são afirmações diferentes; os jobs de código estiveram verdes o tempo todo. Segundo, contadores de regra do iptables (iptables -Z, gerar tráfego, ler -L -v -n) encontraram em cinco minutos o que a leitura de logs não conseguiu: os pacotes me disseram exatamente qual regra os engoliu.

O que vem a seguir

O Dev.to ainda está vazio, então o backfill começa agora: 78 posts, um clique de Publish por vez. Se isso ficar cansativo rápido, tenho um design de staging em lote limitado já esboçado. A revisão também deixou uma lista de follow-up que deliberadamente não incrementei demais neste PR: uma única fonte de verdade syndication_state(provider) em vez de quatro parses de estado espalhados, e um componente de stat-card para o markup copiado e colado do dashboard.

Leitura relacionada

Você também pode achar útil

Proton

Proton Mail

E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
NordPass

NordPass

Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.

Como afiliado da NordPass, ganho com compras qualificadas.

Saiba mais
AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais