<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Automacao | AkitaOnRails.com</title><link>https://akitaonrails.github.io/tags/automacao/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Mon, 19 Oct 2026 23:20:48 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/automacao/index.xml" rel="self" type="application/rss+xml"/><item><title>Estamos no Futuro: Carros voam e não temos mais que fazer HTML/CSS</title><link>https://akitaonrails.github.io/2026/10/16/estamos-no-futuro-carros-voam-e-nao-temos-mais-que-fazer-html-css/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/10/16/estamos-no-futuro-carros-voam-e-nao-temos-mais-que-fazer-html-css/</guid><pubDate>Fri, 16 Oct 2026 02:00:00 -0300</pubDate><description>&lt;p&gt;São mais de duas da manhã e eu ainda estou acordado porque acabei de assistir ao vivo ao novo Tesla Roadster subir uma rampa vertical, parar de cabeça pra baixo e, em outra passagem, girar no ar e pousar inteiro.&lt;/p&gt;
&lt;p&gt;Eu esperava um carro rápido. Não esperava um carro com dez bocais de propulsão a ar comprimido, usando tecnologia derivada da SpaceX, e um sistema de sucção embaixo capaz, &lt;a href="https://paultan.org/2026/10/16/tesla-roadster-revealed-flies-on-spacex-air-thrusters-0-96-km-h-in-0-81s-production-2028/"target="_blank" rel="noopener"&gt;segundo Elon Musk, de puxar até 2 G&lt;/a&gt;. Na demonstração, a Tesla anunciou zero a 60 milhas por hora, quase 97 km/h, em 0,81 segundo. Não divulgou o protocolo da medição.&lt;/p&gt;</description><content:encoded><![CDATA[<p>São mais de duas da manhã e eu ainda estou acordado porque acabei de assistir ao vivo ao novo Tesla Roadster subir uma rampa vertical, parar de cabeça pra baixo e, em outra passagem, girar no ar e pousar inteiro.</p>
<p>Eu esperava um carro rápido. Não esperava um carro com dez bocais de propulsão a ar comprimido, usando tecnologia derivada da SpaceX, e um sistema de sucção embaixo capaz, <a href="https://paultan.org/2026/10/16/tesla-roadster-revealed-flies-on-spacex-air-thrusters-0-96-km-h-in-0-81s-production-2028/"target="_blank" rel="noopener">segundo Elon Musk, de puxar até 2 G</a>. Na demonstração, a Tesla anunciou zero a 60 milhas por hora, quase 97 km/h, em 0,81 segundo. Não divulgou o protocolo da medição.</p>
<p>Era esse o futuro que eu queria ver.</p>
<p>A demonstração inteira está na <a href="https://livestream.tesla.com/"target="_blank" rel="noopener">transmissão oficial da Tesla</a>. A <a href="https://www.roadandtrack.com/news/a74151425/the-air-assisted-tesla-roadster-flies-in-demo-hits-0-60-in-08-seconds/"target="_blank" rel="noopener">Road &amp; Track mediu e descreveu as manobras</a>, e a <a href="https://techcrunch.com/2026/10/15/tesla-reveals-spacex-powered-roadster-that-flies-and-falls-with-style/"target="_blank" rel="noopener">TechCrunch detalhou as três versões</a>: Performance com aerodinâmica ativa e sucção, Thrust com propulsores traseiros, Rocket com sistema de propulsão de dez bocais e apenas um assento.</p>
<p>Claro, o que vimos foi um protótipo numa demonstração controlada. A Tesla prevê iniciar a produção em 2028, e a versão Rocket não está aprovada para uso em vias públicas. Ainda assim, um carro de verdade saiu do chão, girou e pousou. Eu assisti a isso ao vivo do meu escritório. Foi espetacular.</p>
<p>Enquanto Musk faz foguetes pousarem e carros voarem, eu estava fazendo um trabalho bem mais mundano e pequeno: mexendo nos meus sites.</p>
<h2>Seis sites em cerca de um dia<span class="hx:absolute hx:-mt-20" id="seis-sites-em-cerca-de-um-dia"></span>
    <a href="#seis-sites-em-cerca-de-um-dia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes do evento da Tesla, eu passei o dia inteiro redesenhando seis sites:</p>
<ul>
<li><a href="https://ailair.akitaonrails.com/"target="_blank" rel="noopener">Akita&rsquo;s AI Lair</a>, agora combinando com o visual do meu livro novo, <em>Competente o Suficiente</em>;</li>
<li><a href="https://aimemory.sh/"target="_blank" rel="noopener">ai-memory</a>;</li>
<li><a href="https://aijail.io/"target="_blank" rel="noopener">ai-jail</a>;</li>
<li>a newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>;</li>
<li>o <a href="https://blog.themakitachronicles.com/"target="_blank" rel="noopener">arquivo da newsletter</a>;</li>
<li>e este blog, <a href="https://akitaonrails.com/"target="_blank" rel="noopener">AkitaOnRails.com</a>.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/10/redesign-seis-sites-stitch.jpg" alt="Os seis sites redesenhados em produção: Akita’s AI Lair, ai-memory, ai-jail, The M.Akita Chronicles, o arquivo da newsletter e AkitaOnRails"  loading="lazy" /></p>
<p>Todos começaram com variações daquele tema padrão que o Claude faz. Fundo escuro, gradiente roxo ou azul, fonte sans enorme, cartões arredondados, brilho atrás de alguma coisa. Funciona. Também ficou tão comum que hoje você bate o olho e reconhece &ldquo;site feito pelo Claude&rdquo; antes de ler o título.</p>
<p>Eu já tinha tentado pedir designs diferentes pro Claude. Ele trocava a cor, movia os cartões e entregava a mesma gramática visual. Publiquei <a href="https://x.com/AkitaOnRails/status/2110809199890300986"target="_blank" rel="noopener">algumas telas e o resultado da mudança no Twitter</a>. Eu não sou web designer e, por um tempo, aceitei o &ldquo;funciona&rdquo;. Ontem cansei.</p>
<p>Alguém me sugeriu o <a href="https://stitch.withgoogle.com/"target="_blank" rel="noopener">Stitch, do Google</a>. É uma ferramenta experimental do Google Labs que transforma instruções e imagens em interfaces editáveis e código de front-end. A <a href="https://developers.googleblog.com/en/stitch-a-new-way-to-design-uis/"target="_blank" rel="noopener">primeira versão foi apresentada em 2025</a> em cima do Gemini; <a href="https://blog.google/innovation-and-ai/models-and-research/google-labs/stitch-ai-ui-design/"target="_blank" rel="noopener">a versão atual</a> virou uma tela de design com agente, protótipos e exportação de um <code>DESIGN.md</code> que outros agentes conseguem seguir.</p>
<p><a href="https://blog.google/innovation-and-ai/models-and-research/google-labs/stitch-ai-ui-design/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/10/stitch-ai-native-canvas-google.webp" alt="O canvas do Google Stitch com várias interfaces, variações visuais e controles do design system"  loading="lazy" /></a></p>
<p>Criei a conta, peguei o token da API e comecei a trabalhar. O Stitch gerava direções visuais. Claude Code e Codex levavam essas direções pros repositórios reais, adaptavam os componentes, corrigiam responsividade, trocavam imagens, ajustavam SEO, testavam e faziam deploy. Quando a cota de um acabava, eu continuava com o outro.</p>
<p>Os seis sites saem de cinco repositórios. Olhando todos lado a lado, uma coisa ficou clara: o Stitch responde muito melhor quando recebe uma metáfora concreta, o público e o que aquele produto precisa comunicar. O ai-memory virou um arquivo de memória. O ai-jail, uma prisão cinematográfica. O AI Lair adotou uma feira mundial dos anos 1950 e 1960, puxada pela capa do meu livro. A newsletter virou uma transmissão noturna, e este blog, um arquivo editorial. Cinco identidades diferentes em vez de cinco variações do mesmo gradiente do Claude.</p>
<p>O protótipo bonito é a parte fácil. O trabalho só termina quando a linguagem visual chega às páginas internas, ao artigo, ao leitor do livro, ao e-mail, às páginas legais, aos estados de erro e ao celular. Foi isso que os agentes fizeram nos repositórios. No ai-memory, por exemplo, a verificação cobriu 102 páginas em seis idiomas, nos temas claro e escuro, em desktop e celular. No The M.Akita Chronicles, o mesmo design precisou funcionar na inscrição, no arquivo, no detalhe de newsletter, no podcast e no template do e-mail. Um mockup do Stitch não resolve nada disso sozinho.</p>
<p>Foram várias rodadas de protótipos em cada site. Algumas ficaram horrorosas. Outras tinham uma ideia boa cercada de decisões ruins. Eu mandava descartar, preservar uma parte, misturar com outra tentativa e recomeçar. Código ficou barato, então não existe motivo pra se apaixonar pela primeira versão.</p>
<p>O resultado não vai ganhar prêmio de design. Um designer profissional ainda encontraria centenas de coisas pra melhorar. Eu mesmo sei que consigo avançar se gastar mais alguns dias. Só que ficou muito melhor do que estava e, por enquanto, está competente o suficiente.</p>
<p>Seis sites existentes redesenhados, responsivos, com SEO, geração e substituição de imagens, ajustes de HTML, CSS e JavaScript, teste e deploy em produção. Uma pessoa. Cerca de um dia. Um ano atrás, esse pacote seria trabalho de semanas ou meses.</p>
<p>Vamos manter a proporção: são sites pequenos, a maioria landing pages e páginas de conteúdo. Ninguém vai recriar o conjunto inteiro de páginas e sistemas da Amazon.com em um dia. O experimento mostra quanto trabalho repetitivo já saiu da frente, dentro de um escopo que uma pessoa experiente consegue entender e revisar. Projetos grandes continuam grandes, cheios de regras privadas, integrações antigas, estados estranhos e decisões que não cabem num prompt.</p>
<h2>Até o primeiro proofreading pode ser automatizado<span class="hx:absolute hx:-mt-20" id="até-o-primeiro-proofreading-pode-ser-automatizado"></span>
    <a href="#at%c3%a9-o-primeiro-proofreading-pode-ser-automatizado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No mesmo processo eu mandei agentes revisarem os sites como leitores diferentes. Não era correção gramatical. Eu queria saber o que uma pessoa entenderia nos primeiros segundos, onde ficaria perdida, qual promessa pareceria suspeita e se conseguiria completar uma tarefa sem já conhecer o produto.</p>
<p>No ai-jail, usei duas sessões isoladas. Uma assumiu o papel de um engenheiro cético que aprova todos os prompts e nunca tinha ouvido falar do projeto. A outra era gerente de oito desenvolvedores, sem ser engenheira, tentando decidir se deveria aprovar a ferramenta na empresa. No ai-memory foram três: um engenheiro aprendendo ferramentas de IA, uma gerente avaliando um piloto e um pai sem experiência com agentes, preocupado com AGI e empregos. Cada um leu o site sem ver o relatório dos outros.</p>
<p>Qualquer pessoa consegue fazer o mesmo. Suba a aplicação localmente ou abra um deploy de preview, comece sessões novas e dê a cada agente uma tarefa concreta. Um prompt básico pode ser assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Abra este site em desktop e celular: [URL].
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Você é [perfil], conhece [contexto] e nunca viu este produto.
</span></span><span class="line"><span class="cl">Seu objetivo é [decisão ou tarefa concreta].
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Navegue como essa pessoa. Não leia código nem documentação interna antes.
</span></span><span class="line"><span class="cl">Registre:
</span></span><span class="line"><span class="cl">- o que entendeu nos primeiros dez segundos;
</span></span><span class="line"><span class="cl">- o que acha que o produto faz e para quem serve;
</span></span><span class="line"><span class="cl">- onde não soube qual era o próximo passo;
</span></span><span class="line"><span class="cl">- promessas contraditórias, absolutas ou difíceis de acreditar;
</span></span><span class="line"><span class="cl">- termos que o site usa antes de explicar;
</span></span><span class="line"><span class="cl">- problemas de leitura, teclado, contraste, toque e responsividade;
</span></span><span class="line"><span class="cl">- o que impediria você de testar, comprar ou aprovar.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Para cada achado, mostre a página e a evidência visível.
</span></span><span class="line"><span class="cl">Separe bug factual, problema de uso e preferência subjetiva.
</span></span><span class="line"><span class="cl">Não proponha redesign geral. Sugira a menor mudança que resolve o problema.</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Eu prefiro três sessões independentes a uma mesa-redonda de agentes. Se um lê o relatório do outro, começa a concordar por inércia. Depois uma sessão separada consolida os pontos repetidos, descarta gosto pessoal e confere cada afirmação contra o código e a documentação do produto. A revisão pode apontar que uma frase está errada; ela não tem autoridade pra inventar a frase correta.</p>
<p>Os resultados mais úteis costumam cair em quatro grupos: posicionamento, ordem da informação, caminho até a primeira ação e confiança. Testes automáticos tradicionais continuam cuidando de link quebrado, contraste, acessibilidade, overflow e estado de erro. A persona mostra que o botão tecnicamente acessível apareceu cinco telas depois de onde a pessoa precisava dele.</p>
<p>No <a href="https://github.com/akitaonrails/ai-jail-web/blob/main/docs/research/PROOFREAD-2026-10-15-BRIEF.md"target="_blank" rel="noopener">proofreading do ai-jail</a>, os dois perfis concordaram que o site demorava pra mostrar o risco concreto e falava cedo demais como documentação de engenheiro. A ameaça de uma página envenenada foi puxada pra logo depois do hero. Entraram as seções &ldquo;O que muda no primeiro dia&rdquo; e &ldquo;Para quem precisa aprovar isto&rdquo;, com custo operacional, limites e as chaves que uma empresa realmente expõe. A revisão também encontrou uma afirmação tecnicamente errada repetida em várias páginas: a rede não fica inteiramente desligada por padrão. O host da própria API do agente e, no Linux, os registries continuam acessíveis. Esse tipo de leitura também encontra bug de documentação.</p>
<p>O <a href="https://github.com/akitaonrails/ai-memory-web/blob/main/docs/research/persona-review.md"target="_blank" rel="noopener">relatório do ai-memory</a> encontrou permissões descritas de formas contraditórias, promessas de privacidade absolutas demais e a explicação básica soterrada por features avançadas. Na segunda rodada, os perfis perceberam que a instrução de acesso remoto mandava usar HTTP antes de exigir TLS, que um <code>curl</code> sem autenticação prometia a resposta errada e que escolhas importantes apareciam tarde demais no celular. O guia de coleta saiu de aproximadamente 4.364 pixels do topo pra 1.617; os caminhos pra indivíduo ou equipe, de 5.300 pra 2.489. Isso é uma melhoria objetiva nascida de leitura simulada.</p>
<p>No <a href="https://github.com/akitaonrails/ai-toolkit-web/pull/4"target="_blank" rel="noopener">redesign do AI Lair</a>, a revisão para leitores novos reorganizou o caminho de entrada. Uma <a href="https://github.com/akitaonrails/ai-toolkit-web/commit/c2a5237e876f662a8b53450f23f0162fdc1d4a76"target="_blank" rel="noopener">revisão seguinte</a> deixou explícito que o meu setup com vários agentes e dois monitores é uma configuração madura, não requisito pra começar. Uma pessoa, um projeto e um agente bastam. Também surgiu um roteiro de piloto pra gerente: tarefa reversível, dado não sensível, critério de aceitação, prazo, orçamento e medição. A auditoria seguinte encontrou ARIA inválido e uma animação que escondia o texto de abertura de tecnologias assistivas; a <a href="https://github.com/akitaonrails/ai-toolkit-web/pull/5"target="_blank" rel="noopener">correção</a> levou o axe-core de quatro violações sérias pra zero.</p>
<p>Personas simuladas ainda ficam muito abaixo de feedback humano. Carregam os mesmos vícios dos modelos, podem concordar com o prompt e nunca sentem a irritação de quem perdeu vinte minutos, nem assumem o risco de colocar o próprio cartão de crédito. Os relatórios servem como hipóteses. Um usuário real sempre encontra o caso que eu, o agente e a suíte de testes não imaginaram.</p>
<p>Só que antes do primeiro usuário existir, a alternativa costuma ser nenhuma revisão. Duas ou três leituras independentes já pegam promessa contraditória, jargão sem explicação, informação enterrada e fluxo que só faz sentido pra quem escreveu. Rodar essas leituras custa pouco e deixa o produto menos burro antes de chegar em gente de verdade.</p>
<h2>HTML nunca foi programação<span class="hx:absolute hx:-mt-20" id="html-nunca-foi-programação"></span>
    <a href="#html-nunca-foi-programa%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Durante anos eu vi gente fazendo uma ginástica danada pra defender que &ldquo;HTML é linguagem de programação&rdquo; e, portanto, quem escreve HTML é programador.</p>
<p>HTML é marcação: descreve a estrutura de um documento. CSS descreve apresentação. Isso tem valor, exige conhecimento e pode ficar terrivelmente complexo. Complexidade acidental não transforma marcação em programação.</p>
<p>E não há vergonha nenhuma nisso. Comecei a fazer sites nos anos 1990. Recebia um layout feito no Photoshop e transformava aquilo em HTML. Recortava imagens, montava tabela, ajustava pixel, abria em vários navegadores, voltava e tentava de novo. Foi trabalho honesto e pagou minhas contas.</p>
<p>Também era um trabalho basicamente mecânico: copiar, colar, ajustar, olhar, repetir. Exatamente a categoria que computadores deveriam automatizar.</p>
<p>Hoje eu ainda escolhi direção visual, descartei propostas ruins, decidi o que cada site precisava comunicar e reconheci quando o resultado estava bom o suficiente. Os agentes fizeram o serviço braçal. É assim que deveria ter sido há anos.</p>
<h2>A bolha atrasou a automação<span class="hx:absolute hx:-mt-20" id="a-bolha-atrasou-a-automação"></span>
    <a href="#a-bolha-atrasou-a-automa%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Durante boa parte dos anos 2010, <a href="https://fred.stlouisfed.org/series/EFFR"target="_blank" rel="noopener">os juros americanos ficaram perto de zero</a>. Dinheiro barato irrigou fundos, startups e empresas grandes. Contratar mais gente era fácil, e o mercado absorvia qualquer quantidade de trabalho manual disfarçado de engenharia.</p>
<p>Juro baixo não explica sozinho a expansão da tecnologia, mas uma área em recorde de contratação tem pouco incentivo pra eliminar o próprio trabalho repetitivo. Se você consegue colocar mais cinquenta pessoas pra montar tela, coloca mais cinquenta pessoas pra montar tela.</p>
<p>Quem assistiu às minhas palestras em eventos de tecnologia nos anos 2010 deve lembrar de <strong>Restrição == Inovação</strong>. O nome correto era com dois sinais de igual mesmo. Apresentei essa palestra na DevConf 2019, os <a href="https://www.slideshare.net/slideshow/devconf-2019-so-carlos/143923367"target="_blank" rel="noopener">83 slides estão no meu SlideShare</a>, e eu já tinha escrito a mesma tese em 2013, em <a href="https://startupi.com.br/restricoes-sao-libertadoras-menos-e-mais/"target="_blank" rel="noopener">&ldquo;Restrições são libertadoras: menos é mais&rdquo;</a>.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/V0D3PuTLOZ8"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>É a velha máxima: <strong>a necessidade é a mãe da invenção</strong>. Restrição obriga a escolher. Escolha obriga a priorizar. Quando o caminho convencional não cabe no dinheiro, no prazo ou na capacidade disponível, alguém precisa encontrar outro caminho. É daí que costuma sair inovação de verdade.</p>
<p>Dinheiro financia pesquisa cara, compra equipamento e dá tempo pra experimentar. Sobra de dinheiro sem pressão por resultado faz outra coisa: esconde ineficiência com força bruta. Não precisa automatizar, simplificar nem descobrir uma solução melhor enquanto ainda der pra contratar mais gente e aumentar a planilha. Abundância demais transforma problema técnico em despesa operacional e empurra a decisão difícil pra depois.</p>
<p>Eu <a href="/2026/02/08/rant-ia-acabou-com-programadores/">repeti esse aviso no começo de 2026</a>: a bolha de programação conseguiu substituir eficiência por volume. A restrição voltou quando o dinheiro barato acabou. Coincidentemente, os agentes chegaram na mesma hora e deram às empresas uma maneira prática de automatizar o trabalho mecânico que elas passaram uma década escalando com contratação.</p>
<p>Foi assim que a gente passou uma década recriando o mesmo painel administrativo e chamando de inovação. Toda semana aparecia a próxima salvação do front-end: React, Angular, Vue, Svelte, outro <em>bundler</em>, outro framework em cima do framework. São ferramentas úteis em seus contextos, mas nenhuma atacava a pergunta óbvia: por que um ser humano ainda está escrevendo à mão a milésima variação do mesmo formulário?</p>
<p>O estouro da bolha de contratação em 2022 encerrou a festa. No fim daquele ano, o ChatGPT levou modelos de linguagem ao público. GPT-2, só pra corrigir a cronologia, tinha saído em 2019. Quatro anos depois do ChatGPT, programação agêntica virou ferramenta normal de trabalho.</p>
<p>Qualquer tarefa que consiste em copiar padrão, adaptar exemplo, rodar, olhar o erro e tentar de novo já está no alcance desses agentes. Front-end é o alvo mais visível porque acumulou uma quantidade absurda desse trabalho. O resto da programação não está protegido por alguma aura mística. Tem muito back-end que é CRUD, cola entre APIs e configuração repetida. Também será automatizado.</p>
<p>Pra mim, ótimo. Programar nunca foi digitar código. Programar é fazer um computador executar algo útil.</p>
<h2>VS Code é o novo cartão perfurado<span class="hx:absolute hx:-mt-20" id="vs-code-é-o-novo-cartão-perfurado"></span>
    <a href="#vs-code-%c3%a9-o-novo-cart%c3%a3o-perfurado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu escrevi exatamente sobre isso em abril, em <a href="/2026/04/11/vs-code-e-o-novo-cartao-perfurado/">&ldquo;VS Code é o novo Cartão Perfurado&rdquo;</a>.</p>
<p>Por décadas, a interface entre intenção e computador mudou. Primeiro você ligava fios e chaves. Depois perfurava cartões. Depois escrevia Assembly, C, linguagens de alto nível, frameworks e configuração. Cada geração olhou pra anterior e automatizou uma camada de trabalho manual.</p>
<p>Ninguém em sã consciência defenderia hoje que operar uma perfuradora de cartões era a essência da ciência da computação. Era a interface disponível. Digitar cada tag e cada regra de CSS no VS Code também era a interface disponível.</p>
<p>Agora eu descrevo a intenção, mostro referências, avalio o resultado e mando o agente corrigir. Ainda preciso entender como navegador, rede, cache, acessibilidade, semântica, responsividade e SEO funcionam. Sem fundamento eu nem saberia dizer se o agente entregou uma solução ou uma bomba-relógio bonita. O teclado perdeu importância; julgamento ganhou.</p>
<p>Hoje, todos os modelos Tier A que eu testo, de Claude a Kimi, conseguem executar com confiabilidade o trabalho mecânico de front-end que apareceu nesses seis projetos. Eu consigo afirmar, sem hesitar, que qualquer tarefa de front-end que só transforma uma decisão já tomada em código é automatizável. Eles não inventaram seis designs brilhantes sozinhos, porém nenhum precisou que eu escrevesse manualmente o flexbox, o breakpoint ou o componente que já expliquei.</p>
<p>Isso cobre uma parcela enorme do trabalho, mas deixa justamente a parte de maior valor. Alguém ainda precisa conhecer os prós e contras do framework, entender o browser, escolher a arquitetura, perceber quando a biblioteca está sendo usada contra o seu desenho e descobrir por que uma solução aparentemente correta quebra numa combinação específica de aparelho, idioma, estado e dado real. O agente escreve o código; um profissional experiente dá direção, limita o estrago e assume o resultado.</p>
<p>Front-end continua existindo. O excesso de trabalho mecânico e de gente contratada só pra executar receita pronta é que perde valor. Um ótimo profissional de front-end consegue dirigir agentes e entregar muito mais do que entregava sozinho. Um profissional mediano que passou anos confundindo digitação de componente com engenharia vai disputar menos vagas. O mercado fica menor e mais seletivo, com os melhores profissionais produzindo mais. A meta continua a mesma de sempre: chegar perto do topo, onde julgamento e experiência ainda fazem diferença.</p>
<p>E isso não para no browser. <a href="/2026/10/11/existem-linguagens-melhores-pra-programar-com-llms/">Escolha o framework</a>: Android com Flutter ou Kotlin, iOS com Swift e SwiftUI, desktop com Rust/Tauri, Flutter, C# ou Electron. Em geral, agentes modernos conhecem os componentes, as ferramentas de build, os testes e as peculiaridades dessas plataformas. Ainda precisam de direção, feedback e verificação, exatamente como no front-end web. A parte mecânica já é automatizável.</p>
<p>Eu testei isso nos meus próprios projetos pequenos e abertos. O <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">FrankYomik</a> tem um cliente Flutter pra Android e Linux, além do servidor e da extensão. O <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">Frank Sherlock</a> é um app desktop em Rust/Tauri que gera AppImage pra Linux, DMG pra macOS e instalador <code>.exe</code> pra Windows. Também fiz o <a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">Frank Karaoke</a> em Flutter pra Android e o <a href="https://github.com/akitaonrails/frank_mangaplus"target="_blank" rel="noopener">FRANK MANGA+</a> em Rust/Tauri pra desktop. Há vários outros no <a href="https://github.com/akitaonrails?tab=repositories"target="_blank" rel="noopener">meu GitHub</a>.</p>
<p>Nenhum deles saiu perfeito no primeiro prompt. Flutter e Tauri me deram trabalho, especialmente nos primeiros projetos. Confiável quer dizer que o agente consegue avançar, receber uma correção, criar testes, corrigir o que quebrou e chegar num aplicativo funcional sem eu assumir a digitação braçal. Depois de testar web, Android e desktop em stacks diferentes, estou muito satisfeito com o resultado. O padrão se repete: trabalho mecânico virou trabalho de agente.</p>
<p>Às duas da manhã eu tinha agentes fazendo os últimos ajustes neste blog sem perder o ritmo. É disso que eu preciso.</p>
<h2>Quando a web ainda não sabia o que era<span class="hx:absolute hx:-mt-20" id="quando-a-web-ainda-não-sabia-o-que-era"></span>
    <a href="#quando-a-web-ainda-n%c3%a3o-sabia-o-que-era" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No auge da bolha da Internet, designers de carreira estavam criando sites. A web ainda não tinha decidido como uma página deveria funcionar, então havia espaço real pra experimentar. Era feio muitas vezes, pesado quase sempre, mas tinha criação acontecendo.</p>
<p>O iPhone abriu outra janela em 2007. De repente tudo precisava caber numa tela pequena, responder ao toque e funcionar sem mouse. Vieram anos de experimentação até o mercado formar uma gramática razoavelmente estável de navegação, gesto, formulário e conteúdo.</p>
<p>Hoje ninguém precisa redescobrir um menu, uma lista, um checkout ou uma página de artigo. Os padrões estão documentados, milhões de exemplos estão públicos e os modelos viram mais interfaces do que qualquer pessoa conseguiria estudar durante a vida.</p>
<p>O trabalho mais valioso continua existindo: entender o produto, a pessoa que vai usá-lo, as restrições e o que merece aparecer primeiro. O trabalho de converter uma decisão pronta em centenas de linhas previsíveis virou candidato perfeito à automação.</p>
<h2>Empregos desaparecem. Outros aparecem<span class="hx:absolute hx:-mt-20" id="empregos-desaparecem-outros-aparecem"></span>
    <a href="#empregos-desaparecem-outros-aparecem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O mundo nunca ficou parado pra preservar uma função.</p>
<p>Operadores de cartão perfurado digitavam dados e programas em papel. Telefonistas conectavam chamadas fisicamente. Datilógrafos mantinham escritórios funcionando. Linotipistas montavam linhas de metal quente pra imprimir jornais. A tecnologia reduziu ou eliminou essas ocupações em escala. A <a href="https://www.bls.gov/emp/tables/fastest-declining-occupations.htm"target="_blank" rel="noopener">lista atual de profissões em maior declínio do Bureau of Labor Statistics</a> ainda inclui digitadores, datilógrafos e operadores de telefonia. Ninguém propõe voltar a elas pra proteger emprego.</p>
<p>Ao mesmo tempo apareceram trabalhos que uma pessoa de 1926 nem conseguiria nomear: desenvolvedor mobile, SRE, designer de experiência, criador de vídeo pra Internet, especialista em nuvem, engenheiro de machine learning. A própria web matou trabalho antigo e abriu uma indústria inteira.</p>
<p>A história real é menos limpa do que a caricatura. A <a href="https://www.ilo.org/publications/generative-ai-and-jobs-refined-global-index-occupational-exposure"target="_blank" rel="noopener">OIT mostra que a tecnologia costuma automatizar tarefas sem necessariamente eliminar ocupações inteiras</a>. Algumas funções encolhem, outras mudam e trabalhos novos aparecem. Esse consolo estatístico não resolve a vida de quem ficou obsoleto hoje. A pessoa precisa reagir antes que a média da economia reaja por ela.</p>
<p>Compare 2026 com 1926, três anos antes do crash de 1929. Parece outro planeta, embora um século seja muito pouco na escala da história. Não existe contrato garantindo que a sua função atual sobreviva aos próximos dez anos, muito menos aos próximos quarenta.</p>
<h2>E como um júnior ganha experiência agora?<span class="hx:absolute hx:-mt-20" id="e-como-um-júnior-ganha-experiência-agora"></span>
    <a href="#e-como-um-j%c3%banior-ganha-experi%c3%aancia-agora" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vai ficar mais difícil. A escada antiga começava com alguém pagando um iniciante pra montar tela simples, corrigir CSS e copiar componente. O agente faz boa parte disso mais rápido e barato. Empresa nenhuma tem obrigação de preservar trabalho ineficiente como programa de treinamento.</p>
<p>O iniciante vai ter que construir a própria escada com projetos reais e problemas pelos quais ele seja responsável. Isso inclui ler o código que o agente gerou, quebrar e consertar, estudar documentação e código-fonte, escrever testes e contribuir com projetos abertos. O agente dá a qualquer estudante acesso a uma quantidade de prática que antes exigia uma equipe. Quem usar isso só pra gerar código que não entende vai continuar júnior por muito tempo.</p>
<p>O mercado vai premiar quem fizer esse esforço extra. Sempre premiou. Quando alguém pergunta como o júnior vai ganhar experiência sem as tarefas fáceis, muitas vezes a pergunta escondida é outra: &ldquo;como eu consigo um salário melhor sem passar pelo trabalho difícil de ficar bom?&rdquo; A vaga fácil, com salário crescente e pouca cobrança, foi uma distorção da bolha. Adaptar-se inclui aceitar que a entrada ficou mais competitiva.</p>
<h2>Minha carreira nunca ficou no mesmo lugar<span class="hx:absolute hx:-mt-20" id="minha-carreira-nunca-ficou-no-mesmo-lugar"></span>
    <a href="#minha-carreira-nunca-ficou-no-mesmo-lugar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="/2023/02/14/akitando-138-meu-comeco-de-carreira-durante-a-bolha-1997-a-2002-bug-do-milenio-crash/">Comecei com multimídia e sites estáticos</a>. Depois trabalhei em agência digital, consultoria enterprise, integração de SAP, startups, vídeo, educação e, agora, IA. No <a href="/2018/12/26/akitando-34-voce-nao-sabe-nada-de-enterprise-conhecendo-a-sap/">Akitando sobre SAP</a>, eu conto parte dessa fase em que precisei aprender um mundo completamente diferente do que fazia antes.</p>
<p>Atravessei o crash das pontocom, a crise de 2008 e o fim da bolha de juro zero. Sobrevivi porque nunca tratei a minha função do momento como identidade permanente. Continuei estudando, acumulando experiência e mudando de direção.</p>
<p>Uma carreira produtiva não deveria consistir em repetir a mesma tarefa mecânica por décadas. Se o seu valor depende de o mercado continuar pagando alguém pra empurrar tag e pixel à mão, o problema não começou com a IA. O problema foi apostar que o presente seria eterno.</p>
<p>Muita gente entende &ldquo;estar preparado&rdquo; como estudar o mínimo necessário pra vaga aberta hoje. Essa é a receita mais rápida pra ficar obsoleto. Faça o trabalho de curto prazo, mas estude além dele. Aprenda as coisas que parecem inúteis agora. Fundamentos, sistemas operacionais, redes, bancos de dados, negócio, economia, história. Você não sabe qual combinação vai salvar sua carreira daqui a cinco anos.</p>
<p>Eu passei anos falando disso no Akitando. Reveja as playlists <a href="https://www.youtube.com/playlist?list=PLdsnXVqbHDUehzKjiRruy--gncHz9Injy"target="_blank" rel="noopener"><strong>Eu Avisei</strong></a> e <a href="https://www.youtube.com/playlist?list=PLdsnXVqbHDUd6wEL932R7dW9Zazt2oNWF"target="_blank" rel="noopener"><strong>Que curso devo fazer??</strong></a>. Se tiver que começar por um vídeo, comece pelo <strong>Guia DEFINITIVO de Aprendendo a Aprender</strong>. Curso nenhum salva quem ainda terceiriza o próprio aprendizado.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/oUPaJxk6TZ0"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>O mundo não tem falta de oportunidade. A maioria das pessoas sente que nunca recebe uma porque está sempre despreparada quando ela aparece. Quando a oportunidade pousa na sua frente, já é tarde pra começar a preparação.</p>
<p>Portanto, eu não vou lamentar que o pacote desses seis sites pequenos, que antes tomaria semanas ou meses, tenha cabido em cerca de um dia. Estou feliz pra cacete. Ganhei tempo pra pensar no que construir, experimentar mais e jogar fora versões ruins sem desperdiçar semanas de vida.</p>
<p>Hoje eu vi um carro girar no ar e pousar. Em cerca de um dia, coloquei seis sites com visual novo em produção sem escrever a maior parte do HTML e CSS. O futuro que eu queria não chegou perfeito, barato nem distribuído igualmente. Mas chegou o bastante pra eu trabalhar com ele.</p>
<p>Aceite o que acabou. Estude o que vem depois. E esteja pronto antes da próxima oportunidade aparecer.</p>
]]></content:encoded><category>inteligencia-artificial</category><category>frontend</category><category>automacao</category><category>carreira</category></item><item><title>Falando um pouco sobre minhas Skills de IA</title><link>https://akitaonrails.github.io/2026/09/17/falando-um-pouco-sobre-minhas-skills-de-ia/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/09/17/falando-um-pouco-sobre-minhas-skills-de-ia/</guid><pubDate>Thu, 17 Sep 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;De tempos pra cá vários leitores me perguntaram sobre as &amp;ldquo;skills&amp;rdquo; que eu mencionei quando falo do meu workflow com agentes de IA. Este artigo é pra explicar o que são, como eu uso, e qual é a minha recomendação real sobre elas.&lt;/p&gt;
&lt;p&gt;E a recomendação vem logo de cara, porque ela é o motivo do artigo existir: &lt;strong&gt;não use as skills dos outros.&lt;/strong&gt; Lê de novo. As minhas skills estão publicadas no &lt;a href="https://github.com/akitaonrails/my-skills"target="_blank" rel="noopener"&gt;meu repositório my-skills&lt;/a&gt; e a descrição do repo avisa em inglês: &lt;em&gt;&amp;ldquo;not tailored for general usage&amp;rdquo;&lt;/em&gt;. Elas estão lá pra você ler o raciocínio, entender a estrutura, e ir construir as suas. Copiar skill de terceiro e sair usando é o equivalente digital de assinar um contrato sem ler. Pior: é instalar as instruções de outra pessoa pra rodar com as suas permissões, nos seus repositórios, com os seus tokens. Prompt injection você instala voluntariamente e agradece.&lt;/p&gt;</description><content:encoded><![CDATA[<p>De tempos pra cá vários leitores me perguntaram sobre as &ldquo;skills&rdquo; que eu mencionei quando falo do meu workflow com agentes de IA. Este artigo é pra explicar o que são, como eu uso, e qual é a minha recomendação real sobre elas.</p>
<p>E a recomendação vem logo de cara, porque ela é o motivo do artigo existir: <strong>não use as skills dos outros.</strong> Lê de novo. As minhas skills estão publicadas no <a href="https://github.com/akitaonrails/my-skills"target="_blank" rel="noopener">meu repositório my-skills</a> e a descrição do repo avisa em inglês: <em>&ldquo;not tailored for general usage&rdquo;</em>. Elas estão lá pra você ler o raciocínio, entender a estrutura, e ir construir as suas. Copiar skill de terceiro e sair usando é o equivalente digital de assinar um contrato sem ler. Pior: é instalar as instruções de outra pessoa pra rodar com as suas permissões, nos seus repositórios, com os seus tokens. Prompt injection você instala voluntariamente e agradece.</p>
<h2>O que é uma skill, de verdade<span class="hx:absolute hx:-mt-20" id="o-que-é-uma-skill-de-verdade"></span>
    <a href="#o-que-%c3%a9-uma-skill-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não tem mágica nenhuma. Uma skill é um diretório com um arquivo <code>SKILL.md</code> dentro: um cabeçalho com nome e descrição, e um corpo de texto que é, essencialmente, um prompt bem escrito sobre como executar uma tarefa. Às vezes acompanha um <code>scripts/</code> com scripts de apoio, pra que o LLM não precise recriar do zero toda vez.</p>
<blockquote>
  <p>Skills são só prompts.</p>

</blockquote>
<p>É só isso. Um prompt que você versionou, refinou, e deixou a um comando de distância. A única diferença entre uma skill e aquele prompt gigante que você cola no chat todo dia é que a skill mora num arquivo, com nome, e o agente sabe que ela existe.</p>
<p>E aqui vai a resposta pra pergunta que todo mundo faz: &ldquo;mas isso não enche o contexto de porcaria?&rdquo;. A boa notícia é que os harnesses fazem isso direito. O Claude Code usa o que a Anthropic chama de <a href="https://code.claude.com/docs/en/skills"target="_blank" rel="noopener">progressive disclosure</a>: no início da sessão, só o nome e a descrição de cada skill entram no prompt do sistema. O corpo do <code>SKILL.md</code> só é carregado quando o agente decide invocar a skill. O <a href="https://opencode.ai/docs/skills/"target="_blank" rel="noopener">OpenCode faz igual</a>: lista nome e descrição, e carrega o conteúdo sob demanda via ferramenta própria. A própria Anthropic <a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills"target="_blank" rel="noopener">documenta o desenho</a>: metadados primeiro, corpo só quando precisa.</p>
<p>Isso resolve o custo do texto, mas resolve só metade do problema. Cada skill instalada ainda ocupa uma linha na listagem, e mais importante: ela disputa a atenção do agente na hora de decidir qual ferramenta usar. A Anthropic de novo, <a href="https://www.anthropic.com/engineering/writing-tools-for-agents"target="_blank" rel="noopener">no blog de engineering</a>: <em>&ldquo;more tools don&rsquo;t always lead to better outcomes&rdquo;</em>, mais ferramentas nem sempre melhoram o resultado, e a recomendação oficial deles é ter poucas ferramentas bem pensadas pra workflows de alto impacto. A minha versão dessa regra é mais ácida:</p>
<blockquote>
  <p>Skill encostada é dívida.</p>

</blockquote>
<p>Você muda de processo, a skill envelhece, e um dia o agente executa uma regra que nem faz mais sentido no seu workflow. Menos skills, melhor escritas, revisadas com frequência.</p>
<h2>A única skill de terceiro que sobreviveu aqui<span class="hx:absolute hx:-mt-20" id="a-única-skill-de-terceiro-que-sobreviveu-aqui"></span>
    <a href="#a-%c3%banica-skill-de-terceiro-que-sobreviveu-aqui" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Dito tudo isso, seria hipócrita fingir que eu vivo só de skill caseira. Uma sobreviveu ao meu crivo e roda em todo post deste blog: a <a href="https://github.com/akitaonrails/my-skills/tree/master/humanizer"target="_blank" rel="noopener">Humanizer</a>.</p>
<p>Ela existe porque a comunidade da Wikipedia mantém um catálogo público e brutalmente honesto chamado <a href="https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing"target="_blank" rel="noopener">Signs of AI writing</a>, mantido pelo WikiProject AI Cleanup, que documenta uma a uma os &ldquo;tells&rdquo; de texto gerado por IA: a regra de três, o &ldquo;não apenas X, mas também Y&rdquo;, o puffery, o bullet decorado com emoji. A Humanizer pega esse catálogo e vira um procedimento: o agente recebe o texto, marca os tells do mais forte pro mais fraco, reescreve preservando cada alegação suportada (perder fato é erro, inventar fato é erro), e faz uma checagem final lendo em voz alta. Sem alterar o que foi dito, só como foi dito.</p>
<p>Se você produz conteúdo, entende por que eu insisto: texto com cheiro de IA está virando o novo &ldquo;Dear Sir&rdquo;, marca de formulário apressado. Eu rodo a Humanizer em cada artigo antes de publicar, inclusive <a href="https://akitaonrails.com/2026/08/16/entendendo-ia-watermark-da-anthropic/"target="_blank" rel="noopener">neste aqui</a>. Mas nota o detalhe: eu incorporei a versão 3.0.0 no meu repo, com os meus ajustes. Eu li o material de origem antes de confiar. É esse o padrão.</p>
<h2>O que eu faço com isso todos os dias<span class="hx:absolute hx:-mt-20" id="o-que-eu-faço-com-isso-todos-os-dias"></span>
    <a href="#o-que-eu-fa%c3%a7o-com-isso-todos-os-dias" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora a parte prática. Eu mantenho e monitoro mais de 40 repositórios no GitHub. Projetos próprios, forks que eu mantenho, ferramentas que viraram dependência do meu dia. Pra não enlouquecer, meu segundo monitor roda o <a href="https://github.com/akitaonrails/clock-tui"target="_blank" rel="noopener">tclock</a> (meu fork do <a href="https://github.com/race604/clock-tui"target="_blank" rel="noopener">clock-tui</a>, com temas de Evangelion, porque sim) e junto dele o <a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a>, um CLI em Rust que lista issues e PRs abertos de todos os meus repos de uma vez.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260917161027_screenshot-2026-09-17_16-10-27.png" alt="Meu segundo monitor com tclock e ghpending listando os repositórios com PRs e issues abertos"  loading="lazy" /></p>
<p>Na foto dá pra ver: <a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a> e <a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a> com PRs e issues abertos. Todo dia de manhã eu abro o harness em cima de cada projeto com <code>ai-memory run</code> no diretório dele. O ai-memory merece um parágrafo: é o meu projeto de memória de longo prazo pra agentes, um wiki Markdown persistente que captura observações das sessões, consolida decisões e gotchas em páginas, mantém workstreams entre sessões e handoffs entre agentes. Cada projeto meu tem a própria memória, com as decisões arquiteturais e as pegadinhas que só aquele código tem. Sem isso, cada sessão de agente começa do zero como um estagiário com amnésia.</p>
<p>Daí o ritual diário é quase infantil de simples. Eu escrevo, literalmente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">run pr-audit and iss-audit, then run github-resolution</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando a release está madura, adiciono <code>then run the release skill</code>. Nos dias em que só tem bump automático do Dependabot enfileirado, é <code>run pr-bump and release</code> e acabou. Uma linha de instrução minha. O trabalho pesado mora nas skills. Vamos a elas.</p>
<h3>pr-audit<span class="hx:absolute hx:-mt-20" id="pr-audit"></span>
    <a href="#pr-audit" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Auditoria de PR antes do merge, e o princípio central é: <strong>evidence over narrative</strong>. Nada do que o contribuidor escreveu é tratado como verdade, tudo é alegação a verificar, com ledger de claims (confirmado, parcial, sem suporte). O resto da skill é um checklist hostil:</p>
<ul>
<li><strong>Gate de mudanças</strong>: inventário de hunks procurando bit executável, symlink, caractere bidirecional, homoglyph, workflow <code>pull_request_target</code>, action sem pin, dependência typosquat, drift de lockfile.</li>
<li><strong>Credenciais</strong>: acesso a credencial sem explicação é bloqueante na hora. Execução acontece em worktree isolado, sem credencial de host.</li>
<li><strong>Semver</strong>: a categoria do changelog é gate de versão, breaking change anda em major, nunca em patch. Regra que me salvou mais de uma vez.</li>
<li><strong>Prompt injection</strong>: texto dentro do PR que tenta &ldquo;mudar as regras da auditoria&rdquo; é só mais um dado suspeito pra investigar. É achado de auditoria, óbvio.</li>
</ul>
<h3>iss-audit<span class="hx:absolute hx:-mt-20" id="iss-audit"></span>
    <a href="#iss-audit" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A mesma desconfiança, aplicada a issues:</p>
<ul>
<li>Nunca executar comando copiado de issue. Anexo só passa por scanner isolado.</li>
<li>Separação formal entre o observado, o esperado, o diagnóstico de quem reportou e a correção proposta.</li>
<li>Reprodução com dado sintético em sandbox descartável, com limite de recurso pra alegação de DoS.</li>
</ul>
<p>E duas regras de ouro que eu adoro: <em>&ldquo;um número que se move não é um número travado&rdquo;</em> (lag assíncrono parece bug) e <em>&ldquo;o dono óbvio pode ser inocente&rdquo;</em> (query a população suspeita; se vier vazia, a causa está em outro lugar).</p>
<h3>github-resolution<span class="hx:absolute hx:-mt-20" id="github-resolution"></span>
    <a href="#github-resolution" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A fase de execução: transformar o que a auditoria aprovou em código mergeado. As regras:</p>
<ul>
<li>Só executa ticket aprovado, um por vez, com teste de regressão escrito antes da correção. <em>&ldquo;Slop is a defect&rdquo;</em>: sem abstração especulativa, sem TODO, sem refactor drive-by, sem teste enfraquecido.</li>
<li>Se eu resolvi mais de três tickets na sessão, roda o post-audit do intervalo inteiro antes do push, sem exceção.</li>
<li>Ticket fecha quando o fix está mergeado com CI verde naquele SHA exato, nunca fica pendurado esperando release.</li>
<li>Toda issue não resolvida fica registrada com o motivo. Comprimir pra um número é proibido.</li>
</ul>
<h3>pr-bump<span class="hx:absolute hx:-mt-20" id="pr-bump"></span>
    <a href="#pr-bump" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O fast path pros bumps de dependência do Dependabot. A regra que mais economiza meu tempo: <strong>batch, nunca serialize</strong>. Em vez de mergear PR por PR, um <code>bundle update</code> consolidado até a última versão compatível, um único passe de CI pro conjunto, fix forward no que quebrar.</p>
<p>Mas tem um <strong>supply-chain floor</strong>:</p>
<ul>
<li>Toda dependência tem que resolver no registry público (rubygems, npm, pypi) com nome, versão e checksum esperados.</li>
<li>Fonte git, path, nome typosquat-adjacente, hook novo de install: para tudo e vai pra auditoria completa.</li>
</ul>
<p><code>bundler-audit</code> passa numa gem trojanizada bem formada. É exatamente o caso que o floor cobre.</p>
<h3>release<span class="hx:absolute hx:-mt-20" id="release"></span>
    <a href="#release" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Corta versão só quando eu peço. As regras de ferro:</p>
<ul>
<li>Versão sai da classificação do changelog: fix vai em patch, aditivo em minor, breaking em major. Número que conflita com a classificação me obriga a confirmar, bump silencioso é proibido.</li>
<li>Gate de CI verde no SHA exato da tag. Nunca tag em CI pendente ou pulado.</li>
<li>Tag anotada, changelog atualizado.</li>
<li>Nunca reescrever tag publicada. Nunca cortar release que ninguém pediu.</li>
</ul>
<h2>Os números, porque números não discutem<span class="hx:absolute hx:-mt-20" id="os-números-porque-números-não-discutem"></span>
    <a href="#os-n%c3%bameros-porque-n%c3%bameros-n%c3%a3o-discutem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Contando agora, no dia em que escrevo este artigo, só em três projetos (<a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a>, <a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a> e <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>):</p>
<ul>
<li>ai-memory: 430 PRs mergeados, 260 issues fechadas</li>
<li>ai-usagebar: 137 PRs mergeados, 34 issues fechadas</li>
<li>ai-jail: 42 PRs mergeados, 81 issues fechadas</li>
</ul>
<p>Somando: <strong>609 PRs mergeados e 375 issues fechadas</strong>, mais de mil itens. Quase a totalidade disso foi tratada por essas skills, comigo supervisionando e decidindo. Uma pessoa sozinha, sem time, mantendo três projetos ativos nesse volume. É esse o multiplicador.</p>
<h2>E elas não nasceram prontas<span class="hx:absolute hx:-mt-20" id="e-elas-não-nasceram-prontas"></span>
    <a href="#e-elas-n%c3%a3o-nasceram-prontas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Abre o <a href="https://github.com/akitaonrails/my-skills/commits/master"target="_blank" rel="noopener">histórico de commits do my-skills</a> e você vê skill sendo lapidada na marra. Em julho começa o repo com as auditorias básicas. No fim de julho: <em>&ldquo;harden audit skills and add security audit&rdquo;</em>. Em agosto: <em>&ldquo;consolidate agent skills from all harnesses into this repo&rdquo;</em> e, dias depois, nasce a github-resolution como fase de execução das auditorias. Em setembro: <em>&ldquo;fold in release-discipline and audit lessons&rdquo;</em>.</p>
<p>E ontem mesmo, olha o nível do ajuste fino no pr-bump: <em>&ldquo;batch all bump PRs into one consolidated verification pass, fix forward on failures&rdquo;</em> e <em>&ldquo;require deferred/declined PRs be left in a clean state&rdquo;</em>. Duas regras que viraram texto de skill. Nenhuma delas saiu de blog post ou de opinião de terceiro: saiu de eu tomar na cara o erro que a regra agora impede.</p>
<blockquote>
  <p>Cada linha dessas skills é cicatriz.</p>

</blockquote>
<p>É assim que se escreve prompt bom: prestando atenção no resultado, decidindo ajustar, ajustando de novo.</p>
<h2>Skills são pouco. Conhecimento é muito.<span class="hx:absolute hx:-mt-20" id="skills-são-pouco-conhecimento-é-muito"></span>
    <a href="#skills-s%c3%a3o-pouco-conhecimento-%c3%a9-muito" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora o contraponto, porque eu não quero que você saia daqui achando que a resposta pra tudo é skill. Eu dependo pouco delas. O que carrega meu workflow mesmo é outra coisa.</p>
<p>Cada projeto meu tem seu próprio <code>AGENTS.md</code>, instruções específicas daquele código, e sua memória no ai-memory com decisões e gotchas. E o padrão que eu mais uso no dia a dia nem é skill: é apontar pro exemplo vivo. Quando eu quis AUR automatizado pro clock-tui, eu não escrevi skill de &ldquo;como fazer pacote AUR&rdquo;. Eu abri o harness e falei:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">make a similar github action to build and publish AUR packages
</span></span><span class="line"><span class="cl">as we did in ~/Projects/ai-memory</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O agente foi lá, leu o workflow que funciona, e reproduz o padrão no novo projeto. Skill pra isso seria pior: AUR muda, GitHub Actions muda, o exemplo fica velho. Exemplo vivo em repo que eu mantenho está sempre atualizado por definição, porque quando ele quebra, eu conserto.</p>
<blockquote>
  <p>Cada projeto vira base de conhecimento de si mesmo.</p>

</blockquote>
<p>E isso generaliza, porque harnesses conseguem consumir qualquer base de texto. Meu PC Linux inteiro está documentado e reinstalável a partir de um repo de config meu, e meu homeserver está todo descrito em runbooks. Esses dois não estão no GitHub: moram num servidor Gitea privado que eu mantenho, porque é o tipo de coisa que eu não quero vazada em público. Quando eu quero mexer em casa, eu falo assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">add integration to my Plex server as documented in my homelab runbooks</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Não é skill. É conhecimento organizado, versionado, consultável. Skills são pra <em>procedimento</em> que se repete igual. Conhecimento é pra tudo o mais.</p>
<h2>A recomendação que vale mais que qualquer skill<span class="hx:absolute hx:-mt-20" id="a-recomendação-que-vale-mais-que-qualquer-skill"></span>
    <a href="#a-recomenda%c3%a7%c3%a3o-que-vale-mais-que-qualquer-skill" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você levar uma única prática deste artigo, que seja esta: <strong>faça seu agente escrever tudo o que não virou código.</strong> Pesquisa que ele fez, decisão que você tomou e por quê, comparação com alternativa rejeitada. Sem isso, esse conhecimento evapora no fim da sessão.</p>
<p>O meu ai-memory, por exemplo, tem <a href="https://github.com/akitaonrails/ai-memory/tree/main/docs"target="_blank" rel="noopener">uma pasta de docs</a> com pesquisa de competidores que eu mandei fazer antes de cada decisão grande:</p>
<ul>
<li><a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/research-karpathy-llm-wiki.md"target="_blank" rel="noopener">O padrão do wiki do Karpathy</a>, que é a origem intelectual do projeto.</li>
<li><a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/research-2026-landscape.md"target="_blank" rel="noopener">Survey do landscape 2026</a>, cobrindo Zep/Graphiti, Letta, Mem0 e o Open Knowledge Format do Google.</li>
<li>Deep-dives de cada concorrente: <a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/research-hindsight.md"target="_blank" rel="noopener">o Hindsight</a> (o único com paper e funding, que acabou validando minhas apostas de design), <a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/research-cognee.md"target="_blank" rel="noopener">o cognee</a> (pipeline de grafo bonito, bug de gateway feio), <a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/research-agentmemory.md"target="_blank" rel="noopener">o agentmemory</a> (do qual eu guardei as ideias e joguei fora o substrato), <a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/research-basic-memory.md"target="_blank" rel="noopener">o basic-memory</a>.</li>
<li>E a minha favorita, a autópsia do <a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/issues-mempalace.md"target="_blank" rel="noopener">MemPalace</a>: 47 mil stars, viral, e o tracker entupido de corrupção de índice e dado perdido. O conto de fadas ao contrário que me confirmou por que compilar memória é melhor que estocar verbatim.</li>
</ul>
<p>Dezenas de documentos assim. Cada hora de pesquisa que um agente fez pra mim virou ativo permanente, reutilizável em qualquer projeto novo. Quanto mais você produz e documenta, mais inteligência você acumula.</p>
<blockquote>
  <p>É juro composto de conhecimento.</p>

</blockquote>
<h2>Ninguém sabe o que você precisa<span class="hx:absolute hx:-mt-20" id="ninguém-sabe-o-que-você-precisa"></span>
    <a href="#ningu%c3%a9m-sabe-o-que-voc%c3%aa-precisa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>E aí fecha o círculo, e voltamos ao ponto do começo: por que eu não recomendo copiar skill de ninguém, nem a minha.</p>
<p>Porque a minha skill de auditoria de PR é moldada em <em>meus</em> projetos, <em>minhas</em> linguagens, <em>meus</em> riscos, <em>meu</em> nível de paranóia com supply chain. Ela reflete o que eu, Fabio, mantendo os repositórios que eu mantenho, na infra que eu tenho, com os objetivos que eu tenho, precisei escrever nela. Seu projeto tem outra stack, outros riscos, outro grau de tolerância. Você pode precisar de checagens que eu não preciso, e o inverso. A experiência é relevante só pra quem viveu as restrições dela. Isso muda de pessoa pra pessoa, e é exatamente por isso que skill de terceiro usada às cegas é aposta.</p>
<p>É a tese do meu vídeo de 2019, <a href="/2019/10/09/akitando-63-nao-terceirize-suas-decisoes-a-licao-mais-importante-da-sua-vida/">Não Terceirize suas Decisões: a lição mais importante da sua vida</a>: ninguém além de você sabe o que você precisa.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/D3L8IOncLkg"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Eu não pergunto pra ninguém. Eu pesquiso, eu construo o meu, e as skills são o registro versionado das minhas decisões. A beleza dos LLMs é que eles aceleram esse processo em 10 vezes: a pesquisa que levava uma semana agora leva uma tarde, e o custo de construir sua própria ferramenta despencou.</p>
<p>Skill boa é a sua. A minha está no <a href="https://github.com/akitaonrails/my-skills"target="_blank" rel="noopener">my-skills</a> pra você ler o raciocínio. Depois fecha a aba e escreve a sua. Eu não desperdiço tempo pedindo a opinião dos outros. Você também não deveria.</p>
]]></content:encoded><category>agentes-de-codigo</category><category>automacao</category><category>llms</category></item><item><title>Backup de Gmail pra Maildir em Linux</title><link>https://akitaonrails.github.io/2026/05/28/backup-gmail-maildir-linux/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/05/28/backup-gmail-maildir-linux/</guid><pubDate>Thu, 28 May 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;A maioria das pessoas confia no Google e deixa tudo lá. Quem acompanha o blog sabe que eu não confio em ninguém: se tá na nuvem, não é meu. Por isso tenho um NAS em casa pra fazer backup de tudo que importa. Não confio em Google Drive, OneDrive, Dropbox, e nem em Gmail.&lt;/p&gt;
&lt;p&gt;E-mail sempre foi a parte mais chata desse exercício. Meu workflow antigo era abrir o Thunderbird, deixar ele puxar tudo de Gmail via IMAP, e fazer backup do diretório &lt;code&gt;~/.thunderbird&lt;/code&gt;. Funciona, mas é manual, chato (lembrar de abrir o Thunderbird de vez em quando), e preso ao próprio Thunderbird: se quero ler aqueles e-mails em outro cliente, eles estão num formato interno proprietário.&lt;/p&gt;</description><content:encoded><![CDATA[<p>A maioria das pessoas confia no Google e deixa tudo lá. Quem acompanha o blog sabe que eu não confio em ninguém: se tá na nuvem, não é meu. Por isso tenho um NAS em casa pra fazer backup de tudo que importa. Não confio em Google Drive, OneDrive, Dropbox, e nem em Gmail.</p>
<p>E-mail sempre foi a parte mais chata desse exercício. Meu workflow antigo era abrir o Thunderbird, deixar ele puxar tudo de Gmail via IMAP, e fazer backup do diretório <code>~/.thunderbird</code>. Funciona, mas é manual, chato (lembrar de abrir o Thunderbird de vez em quando), e preso ao próprio Thunderbird: se quero ler aqueles e-mails em outro cliente, eles estão num formato interno proprietário.</p>
<p>Depois de um tempo caiu a ficha: Linux tem suporte nativo a e-mail desde sempre. Os protocolos modernos (IMAP, SMTP) são padrão aberto. E o formato de armazenamento local, <strong>Maildir</strong>, existe e funciona muito bem desde os anos 90. Não precisa de Thunderbird pra nada.</p>
<h2>O que é Maildir<span class="hx:absolute hx:-mt-20" id="o-que-é-maildir"></span>
    <a href="#o-que-%c3%a9-maildir" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Maildir foi criado pelo Daniel J. Bernstein (djb) como parte do qmail em 1995. A ideia era resolver os problemas de file locking do formato mbox, onde a caixa de e-mail inteira é um arquivo único e dois processos escrevendo ao mesmo tempo podiam corromper tudo. No Maildir, <strong>cada mensagem vira um arquivo separado</strong> numa estrutura de três subdiretórios:</p>
<ul>
<li><code>tmp/</code> — onde o e-mail é escrito enquanto chega</li>
<li><code>new/</code> — mensagens novas, ainda não lidas</li>
<li><code>cur/</code> — mensagens já vistas pelo cliente, com flags no nome do arquivo (<code>:2,S</code> = seen, <code>:2,F</code> = flagged, etc.)</li>
</ul>
<p>A entrega é atômica: o MTA escreve a mensagem em <code>tmp/</code>, depois faz um <code>rename()</code> pra <code>new/</code>. <code>rename()</code> em filesystem POSIX é atômico, então não tem janela de corrupção. Sem locking, sem corrupção, e cada e-mail é só um arquivo de texto puro que você pode ler com <code>cat</code>, indexar com <code>grep</code>, versionar com git se quiser.</p>
<p>Trinta anos depois, ainda é o formato local padrão da maioria dos clientes de e-mail Linux. E é o formato que vou usar pra fazer o mirror do Gmail.</p>
<h2>A ferramenta: mbsync (isync)<span class="hx:absolute hx:-mt-20" id="a-ferramenta-mbsync-isync"></span>
    <a href="#a-ferramenta-mbsync-isync" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O que faz a ponte entre o IMAP do Gmail e o Maildir local é o <a href="https://isync.sourceforge.io/"target="_blank" rel="noopener"><code>mbsync</code></a>, o binário do pacote <code>isync</code>. Ele lê uma config simples (<code>~/.config/isyncrc</code>), conecta no IMAP do Gmail, e copia as mensagens pro disco. A diferença pro <code>offlineimap</code> (alternativa mais conhecida) é que <code>mbsync</code> é escrito em C, é bem mais rápido em mailboxes grandes, e tem menos casos esquisitos com Gmail.</p>
<p>A config é direta. Resumo do que tem no meu <code>~/.config/isyncrc</code> (sem senha, claro):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">IMAPAccount minha-conta</span>
</span></span><span class="line"><span class="cl"><span class="na">Host imap.gmail.com</span>
</span></span><span class="line"><span class="cl"><span class="na">User you@example.com</span>
</span></span><span class="line"><span class="cl"><span class="na">PassCmd &#34;/home/akitaonrails/.config/mbsync/bin/mbsync-password you@example.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">TLSType IMAPS</span>
</span></span><span class="line"><span class="cl"><span class="na">CertificateFile /etc/ssl/certs/ca-certificates.crt</span>
</span></span><span class="line"><span class="cl"><span class="na">AuthMechs LOGIN</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="na">IMAPStore minha-remote</span>
</span></span><span class="line"><span class="cl"><span class="na">Account minha-conta</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="na">MaildirStore minha-local</span>
</span></span><span class="line"><span class="cl"><span class="na">SubFolders Verbatim</span>
</span></span><span class="line"><span class="cl"><span class="na">Path ~/.local/share/mail/mbsync/you.example.com/</span>
</span></span><span class="line"><span class="cl"><span class="na">Inbox ~/.local/share/mail/mbsync/you.example.com/INBOX</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="na">Channel minha-backup</span>
</span></span><span class="line"><span class="cl"><span class="na">Far :minha-remote:</span>
</span></span><span class="line"><span class="cl"><span class="na">Near :minha-local:</span>
</span></span><span class="line"><span class="cl"><span class="na">Patterns * !&#34;[Gmail]/Trash&#34; !&#34;[Gmail]/Spam&#34; !&#34;[Gmail]/Drafts&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">Create Near</span>
</span></span><span class="line"><span class="cl"><span class="na">Sync Pull New Flags</span>
</span></span><span class="line"><span class="cl"><span class="na">Expunge None</span>
</span></span><span class="line"><span class="cl"><span class="na">Remove None</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Três pontos importantes nessa config:</p>
<ol>
<li><strong><code>Sync Pull</code></strong>: só puxa do servidor pro local. Nada que eu faça no Maildir vai propagar pro Gmail. Isso é proteção contra acidentes (se eu apagar um arquivo localmente, não some do Gmail).</li>
<li><strong><code>Expunge None</code> + <code>Remove None</code></strong>: se uma mensagem some do Gmail (alguém apagou ou o Google decidiu mover pra lugar nenhum), ela <strong>continua</strong> no meu Maildir. O backup acumula histórico, não espelha o estado atual.</li>
<li><strong><code>PassCmd</code> em vez de senha em texto</strong>: a senha vai num script que faz <code>secret-tool lookup</code> no keyring do desktop. Nada de senha em arquivo de config no disco.</li>
</ol>
<h3>O bloco da senha<span class="hx:absolute hx:-mt-20" id="o-bloco-da-senha"></span>
    <a href="#o-bloco-da-senha" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>mbsync-password</code> é um wrapper minúsculo que isola o segredo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nv">account</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="p">:?usage: mbsync-password ACCOUNT</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">password</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>secret-tool lookup service mbsync account <span class="s2">&#34;</span><span class="nv">$account</span><span class="s2">&#34;</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -z <span class="s2">&#34;</span><span class="nv">$password</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;missing keyring secret for </span><span class="nv">$account</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span>2<span class="p">;</span> <span class="nb">exit</span> 1<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;%s&#39;</span> <span class="s2">&#34;</span><span class="nv">$password</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Senha é armazenada uma vez com <code>secret-tool store --label=&quot;mbsync gmail&quot; service mbsync account you@example.com</code>. O Gmail moderno exige uma <strong>app password</strong> específica (16 caracteres, gerada em <a href="https://myaccount.google.com/apppasswords"target="_blank" rel="noopener">myaccount.google.com/apppasswords</a>) porque não aceita mais autenticação básica direta — você não cola sua senha do Google ali, cola uma chave gerada exclusivamente pro app, que dá pra revogar a qualquer momento sem mexer no resto da conta.</p>
<h3>Automação com systemd user timer<span class="hx:absolute hx:-mt-20" id="automação-com-systemd-user-timer"></span>
    <a href="#automa%c3%a7%c3%a3o-com-systemd-user-timer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra não ter que rodar o mbsync na mão, montei dois user units no systemd. Primeiro o serviço, que é só uma chamada do script wrapper:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># ~/.config/systemd/user/mbsync-backup.service</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Mirror Gmail accounts locally with mbsync</span>
</span></span><span class="line"><span class="cl"><span class="na">Wants</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">%h/.config/mbsync/bin/mbsync-run</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E o timer que dispara o serviço periodicamente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># ~/.config/systemd/user/mbsync-backup.timer</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Run mbsync periodically</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnBootSec</span><span class="o">=</span><span class="s">10m</span>
</span></span><span class="line"><span class="cl"><span class="na">OnUnitActiveSec</span><span class="o">=</span><span class="s">30m</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl"><span class="na">Unit</span><span class="o">=</span><span class="s">mbsync-backup.service</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>OnBootSec=10m</code> espera 10 minutos depois do boot pra primeira execução (evita brigar com o network-online no início da sessão). <code>OnUnitActiveSec=30m</code> re-dispara 30 minutos depois de cada execução terminar. <code>Persistent=true</code> faz com que, se a máquina estava desligada na hora que seria pra disparar, ele compense rodando logo no próximo boot. O wrapper já pega <code>flock</code> pra não rodar dois mbsync em paralelo, então se uma sincronização demorar mais que 30min, a próxima só espera. Tudo loga em <code>~/.local/state/mbsync/mbsync.log</code>.</p>
<p>Pra gerenciar o timer, os comandos básicos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Re-ler os arquivos depois de criar/editar</span>
</span></span><span class="line"><span class="cl">systemctl --user daemon-reload
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ativar agora + habilitar pra subir no próximo boot</span>
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now mbsync-backup.timer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Conferir se o timer tá ativo e quando dispara da próxima vez</span>
</span></span><span class="line"><span class="cl">systemctl --user status mbsync-backup.timer
</span></span><span class="line"><span class="cl">systemctl --user list-timers mbsync-backup.timer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Disparar uma sincronização imediata sem esperar o intervalo</span>
</span></span><span class="line"><span class="cl">systemctl --user start mbsync-backup.service
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Acompanhar logs em tempo real</span>
</span></span><span class="line"><span class="cl">journalctl --user -u mbsync-backup.service -f
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Conferir status da última execução do serviço</span>
</span></span><span class="line"><span class="cl">systemctl --user status mbsync-backup.service
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pra desligar tudo</span>
</span></span><span class="line"><span class="cl">systemctl --user disable --now mbsync-backup.timer</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra que o timer continue rodando quando você não está logado (útil em desktop que fica ligado 24/7), habilite linger uma única vez: <code>sudo loginctl enable-linger $USER</code>. Sem isso, os user units só rodam enquanto você tem sessão ativa.</p>
<p>Ativa, confere o status uma vez pra ter certeza que tá verde, e esquece que existe.</p>
<h2>Backup do Maildir com restic<span class="hx:absolute hx:-mt-20" id="backup-do-maildir-com-restic"></span>
    <a href="#backup-do-maildir-com-restic" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora que o Gmail está espelhado em <code>~/.local/share/mail/</code>, ele entra no mesmo pipeline de backup do resto do meu home. Eu uso o <a href="https://restic.net/"target="_blank" rel="noopener">restic</a> pra mandar snapshots criptografados pro NAS. O restic é incremental por content-addressable storage, então adicionar 100MB de e-mail no snapshot diário custa basicamente o delta — não os 100MB inteiros toda vez. Pra cópia off-site, meu NAS Synology já tem o pacote Glacier Backup nativo, que cuida de mandar snapshots criptografados pro AWS Glacier sem eu precisar configurar nada além do bucket e da chave. O Maildir entra junto no mesmo plano, então o backup off-site sai de graça em termos de configuração extra.</p>
<p>O Maildir é amigável a backup incremental por construção: cada e-mail é um arquivo imutável (nem o nome muda depois de baixado, exceto pela flag de seen/flagged). O restic deduplica perfeitamente. Snapshot de mailbox de 5GB tem custo marginal próximo de zero depois do primeiro.</p>
<h2>A vantagem do formato aberto: muitos clientes<span class="hx:absolute hx:-mt-20" id="a-vantagem-do-formato-aberto-muitos-clientes"></span>
    <a href="#a-vantagem-do-formato-aberto-muitos-clientes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui mora a parte que me convenceu de vez. Maildir é um padrão aberto, então <strong>qualquer cliente que entenda Maildir lê o mesmo backup</strong>. Eu posso:</p>
<ul>
<li>Abrir no <a href="http://www.mutt.org/"target="_blank" rel="noopener">mutt</a> ou <a href="https://neomutt.org/"target="_blank" rel="noopener">neomutt</a> quando quero algo rápido no terminal.</li>
<li>Abrir no <a href="https://aerc-mail.org/"target="_blank" rel="noopener">aerc</a> quando quero TUI mais moderna.</li>
<li>Apontar o Thunderbird ou <a href="https://www.claws-mail.org/"target="_blank" rel="noopener">Claws Mail</a> pra <code>~/.local/share/mail/...</code> quando quero GUI.</li>
<li>Migrar pra outro cliente no futuro sem ter que exportar/importar nada.</li>
</ul>
<p>Os e-mails são meus. Vivem no meu disco. Em formato texto, legível por qualquer ferramenta.</p>
<h2>Bônus: scripts e bots leem direto<span class="hx:absolute hx:-mt-20" id="bônus-scripts-e-bots-leem-direto"></span>
    <a href="#b%c3%b4nus-scripts-e-bots-leem-direto" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A consequência menos óbvia disso é que, se eu quiser construir um filtro, um bot, um classificador, ou qualquer coisa que mexe nos e-mails, eu <strong>não preciso passar por OAuth do Gmail, criar projeto no Google Cloud, gerenciar refresh tokens, lidar com quota</strong> — nada. Eu leio direto do Maildir.</p>
<p>O Python tem suporte nativo a Maildir na biblioteca <code>mailbox</code>. Um exemplo bobo pra listar assuntos da inbox:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">mailbox</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">email.header</span> <span class="kn">import</span> <span class="n">decode_header</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">inbox</span> <span class="o">=</span> <span class="n">mailbox</span><span class="o">.</span><span class="n">Maildir</span><span class="p">(</span><span class="s2">&#34;/home/akitaonrails/.local/share/mail/mbsync/you.example.com/INBOX&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">inbox</span><span class="o">.</span><span class="n">iteritems</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">subject</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;Subject&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">sender</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;From&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># decode_header lida com Subject em UTF-8 / latin-1 / etc</span>
</span></span><span class="line"><span class="cl">    <span class="n">decoded</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">part</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">enc</span> <span class="ow">or</span> <span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">part</span><span class="p">,</span> <span class="nb">bytes</span><span class="p">)</span> <span class="k">else</span> <span class="n">part</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">part</span><span class="p">,</span> <span class="n">enc</span> <span class="ow">in</span> <span class="n">decode_header</span><span class="p">(</span><span class="n">subject</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">sender</span><span class="p">[:</span><span class="mi">40</span><span class="p">]</span><span class="si">:</span><span class="s2">&lt;40</span><span class="si">}</span><span class="s2">  </span><span class="si">{</span><span class="n">decoded</span><span class="p">[:</span><span class="mi">60</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Roda em milissegundos, não consome quota de API nenhuma, não tem rate limit, não precisa renovar credencial. É só leitura de arquivos. Se você quer fazer um pipeline mais sério (parser de anexos, classificação por LLM, regras automáticas), o mesmo princípio se aplica.</p>
<h2>Resumo<span class="hx:absolute hx:-mt-20" id="resumo"></span>
    <a href="#resumo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A combinação que finalmente me deixou tranquilo:</p>
<ul>
<li><strong>mbsync</strong> (isync) puxa do IMAP do Gmail pra Maildir local, automaticamente via systemd timer.</li>
<li><strong>Maildir</strong> preserva o formato aberto, cada e-mail é um arquivo, pull-only protege contra exclusão acidental.</li>
<li><strong>restic</strong> snapshota o Maildir pro NAS com deduplicação ridícula, e o Glacier Backup do Synology leva isso pra off-site cifrado.</li>
<li><strong>Cliente de e-mail livre</strong> — mutt, neomutt, aerc, Thunderbird, Claws, qualquer um lê.</li>
<li><strong>Automação leve</strong> — bot ou script lê direto do disco, sem OAuth, sem API.</li>
</ul>
<p>O Gmail vira só uma interface web confortável pra ler e responder. A propriedade real dos e-mails está no meu disco, replicada no NAS, replicada em backup off-site cifrado. Se o Google fechar minha conta amanhã (já aconteceu com gente), perco a interface, mas mantenho a história inteira. Pra mim é o jeito definitivo de fazer isso. Recomendo.</p>
]]></content:encoded><category>armazenamento-e-backup</category><category>linux</category><category>automacao</category></item><item><title>Criei um CLI pra Checar Pendências no Meu GitHub: ghpending</title><link>https://akitaonrails.github.io/2026/05/23/criei-cli-pra-checar-pendencias-github-ghpending/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/05/23/criei-cli-pra-checar-pendencias-github-ghpending/</guid><pubDate>Sat, 23 May 2026 22:00:00 -0300</pubDate><description>&lt;p&gt;Depois da minha &lt;a href="https://akitaonrails.github.io/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/"&gt;maratona de IA&lt;/a&gt;, fiquei com dezenas de repositórios públicos no GitHub pra cuidar. Vários deles têm contribuidores postando issues e mandando pull requests. O problema é que abrir o navegador, entrar em cada repo, ver se tem alguma coisa nova esperando, vira um ritual cansativo que eu acabo evitando. E quando evito, fico semanas sem responder os colaboradores.&lt;/p&gt;
&lt;p&gt;Sei que tem várias ferramentas que resolvem isso. Mas eu queria algo bem simples e rápido pra rodar no terminal e ver pendências de todos os meus projetos de uma vez só. Então fiz: o &lt;strong&gt;&lt;a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener"&gt;ghpending&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Depois da minha <a href="/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/">maratona de IA</a>, fiquei com dezenas de repositórios públicos no GitHub pra cuidar. Vários deles têm contribuidores postando issues e mandando pull requests. O problema é que abrir o navegador, entrar em cada repo, ver se tem alguma coisa nova esperando, vira um ritual cansativo que eu acabo evitando. E quando evito, fico semanas sem responder os colaboradores.</p>
<p>Sei que tem várias ferramentas que resolvem isso. Mas eu queria algo bem simples e rápido pra rodar no terminal e ver pendências de todos os meus projetos de uma vez só. Então fiz: o <strong><a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a></strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/23/ghpending/ghpending-screenshot.png" alt="Saída do ghpending no terminal: tabela compacta listando cada repo monitorado com contagem de issues e pull requests abertos, autor da última atividade e há quanto tempo."  loading="lazy" /></p>
<p>Roda em Linux e macOS. Eu testei em Linux (Arch + Omarchy) mas ainda não rodei no macOS. Se você for testar lá e topar com algum bug, abre uma issue lá: <a href="https://github.com/akitaonrails/ghpending/issues"target="_blank" rel="noopener">github.com/akitaonrails/ghpending/issues</a>.</p>
<h2>Instalação<span class="hx:absolute hx:-mt-20" id="instalação"></span>
    <a href="#instala%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Três caminhos, escolhe o que mais combina com seu setup.</p>
<p>Via Homebrew (macOS ou Linux):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Via Cargo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">cargo install ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Via mise:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mise use -g github:akitaonrails/ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Atualizando<span class="hx:absolute hx:-mt-20" id="atualizando"></span>
    <a href="#atualizando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># Homebrew</span>
</span></span><span class="line"><span class="cl">brew upgrade ghpending
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Cargo</span>
</span></span><span class="line"><span class="cl">cargo install ghpending --force
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># mise</span>
</span></span><span class="line"><span class="cl">mise upgrade ghpending
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From source</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ghpending <span class="o">&amp;&amp;</span> git pull <span class="o">&amp;&amp;</span> cargo install --path .</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Uso<span class="hx:absolute hx:-mt-20" id="uso"></span>
    <a href="#uso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quatro comandos. O fluxo normal é rodar <code>ghpending add</code> uma vez pra configurar quais repos você quer monitorar, depois é só <code>ghpending</code> quando quiser ver o digest.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ghpending add    <span class="c1"># pick os repos de um usuário ou org pra monitorar</span>
</span></span><span class="line"><span class="cl">ghpending        <span class="c1"># imprime o digest de issues e PRs abertos</span>
</span></span><span class="line"><span class="cl">ghpending list   <span class="c1"># mostra os repos monitorados</span>
</span></span><span class="line"><span class="cl">ghpending rm     <span class="c1"># tira repos do monitoramento via menu interativo</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>add</code> pergunta o usuário ou organização, lista os repos públicos, e deixa você marcar os que quer acompanhar. O username fica salvo, então rodar <code>add</code> de novo já pula essa pergunta.</p>
<p>O comando principal (<code>ghpending</code> sem argumentos) busca todos os repos monitorados em paralelo e mostra a tabela com issues e PRs abertos.</p>
<h2>Token do GitHub (opcional)<span class="hx:absolute hx:-mt-20" id="token-do-github-opcional"></span>
    <a href="#token-do-github-opcional" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sem autenticação funciona, mas o GitHub limita em 60 requisições por hora pra anônimo. Pra subir esse teto pra 5.000 req/h, exporta um token:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">GITHUB_TOKEN</span><span class="o">=</span><span class="k">$(</span>gh auth token<span class="k">)</span> ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ele é lido silenciosamente na inicialização, não precisa configurar nada.</p>
<h2>Onde fica o config<span class="hx:absolute hx:-mt-20" id="onde-fica-o-config"></span>
    <a href="#onde-fica-o-config" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Linux: <code>~/.config/ghpending/config.toml</code></li>
<li>macOS: <code>~/Library/Application Support/ghpending/config.toml</code></li>
</ul>
<p>Você pode editar direto se quiser reordenar ou trocar o <code>user</code>. Estrutura:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">user</span> <span class="p">=</span> <span class="s2">&#34;akitaonrails&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">repos</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;akitaonrails/ai-memory&#34;</span><span class="p">,</span> <span class="s2">&#34;akitaonrails/ai-jail&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Por que esse CLI existe<span class="hx:absolute hx:-mt-20" id="por-que-esse-cli-existe"></span>
    <a href="#por-que-esse-cli-existe" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A real é simples: ferramenta que eu fizesse pra mim mesmo, eu uso. Ferramenta de outro autor, sempre tem fricção (login web, opinião arquitetural diferente, feature que faltou ou sobrou). Como o objetivo era reduzir o atrito de checar 30 repos por semana, melhor 200 linhas de Rust escritas num sábado à tarde do que aprender um stack novo.</p>
<p>Agora espero conseguir responder as pendências dos colaboradores mais rápido. Se você achar útil, é MIT, PRs são bem-vindos.</p>
]]></content:encoded><category>ghpending</category><category>automacao</category><category>ferramentas-de-desenvolvimento</category><category>controle-de-versao</category></item><item><title>Distrobox de Emulação com Claude Code</title><link>https://akitaonrails.github.io/2026/04/11/distrobox-de-emulacao-com-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/04/11/distrobox-de-emulacao-com-claude-code/</guid><pubDate>Sat, 11 Apr 2026 18:00:00 -0300</pubDate><description>&lt;p&gt;Eu gosto de videogame velho desde antes de muita gente aqui nascer. Meu primeiro contato foi ainda na era do Atari, no começo dos anos 80. Depois vieram os micros de 8 bits, os arcades dos anos 90, os consoles de 16, 32, 64 bits, e eu continuei acompanhando tudo. Nostalgia, pra mim, não é pasta bonitinha no Instagram. É acervo mesmo. ROM, BIOS, dumps, patches, DLC, firmware, save. Ao longo dos anos fui guardando tudo no meu NAS. Hoje tenho terabytes disso em &lt;code&gt;/mnt/terachad/Emulators&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu gosto de videogame velho desde antes de muita gente aqui nascer. Meu primeiro contato foi ainda na era do Atari, no começo dos anos 80. Depois vieram os micros de 8 bits, os arcades dos anos 90, os consoles de 16, 32, 64 bits, e eu continuei acompanhando tudo. Nostalgia, pra mim, não é pasta bonitinha no Instagram. É acervo mesmo. ROM, BIOS, dumps, patches, DLC, firmware, save. Ao longo dos anos fui guardando tudo no meu NAS. Hoje tenho terabytes disso em <code>/mnt/terachad/Emulators</code>.</p>
<p>No meu canal eu inclusive usei jogos antigos pra ensinar fundamentos de computação. No <a href="https://www.youtube.com/watch?v=hYJ3dvHjeOE&amp;pp=ygUUYWtpdGFuZG8gc3VwZXIgbWFyaW8%3D"target="_blank" rel="noopener">Akitando sobre Super Mario e computadores antigos</a> eu explico 6502, mapa de memória, PPU, limitações de hardware e por que aqueles jogos eram programados daquele jeito. Se você nunca viu, recomendo assistir:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/hYJ3dvHjeOE"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>O problema é o de sempre: a cada poucos anos eu resolvo montar uma máquina nova de emulação, baixo de novo os emuladores principais da vez, e lá vou eu repetir o mesmo ritual masoquista. <code>PCSX2</code>, <code>RPCS3</code>, <code>Eden</code>, <code>Azahar</code>, <code>Dolphin</code>, <code>RetroArch</code>, <code>Flycast</code>, <code>shadPS4</code> e por aí vai. Cada um com suas manias. Cada um com sua forma particular de te fazer perder horas.</p>
<p>Em teoria, montar tudo isso deveria ser divertido. Na prática, leva dias. E não é exagero. <code>Dolphin</code> ainda consegue ser um dos piores porque controle de GameCube e Wii não tem nada de padrão. <code>RPCS3</code> exige tuning por jogo. <code>Eden</code> precisa de DLC, update e cheats no lugar certo. <code>shadPS4</code> é um festival de tentativa e erro. Quando eu finalmente terminava de configurar tudo, eu já não queria mais jogar porcaria nenhuma. Eu só queria fechar os menus e ir fazer outra coisa.</p>
<p>Eu já cheguei a dizer que talvez a graça estivesse justamente no tuning. Não acho mais isso. Depois de repetir esse processo vezes demais, eu cansei. Eu quero jogar os jogos, não preencher formulário escondido em GUI de emulador.</p>
<h2>O problema não era Linux<span class="hx:absolute hx:-mt-20" id="o-problema-não-era-linux"></span>
    <a href="#o-problema-n%c3%a3o-era-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já resolvi esse tipo de coisa de várias formas no passado. Anos atrás eu rodava Linux no host e um Windows virtualizado com GPU passthrough pra jogar dentro da VM. Na época era o que fazia sentido. Eu fiz um vídeo inteiro sobre isso:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/IDnabc3DjYY"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Mas isso foi antes do trabalho monumental da Valve, do Proton, do Wine moderno e de toda a comunidade open source que empurrou o desktop Linux pra um lugar muito melhor. Hoje dá pra evitar Windows na maior parte dos casos. No outro vídeo abaixo eu explico a evolução de CPU, GPU, OpenGL, Vulkan e por que chegamos nesse ponto:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/JEp7ozWqIps"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Meu mini-PC dedicado a games com RTX 4090 continua sendo minha principal máquina de Steam, especialmente agora que montei um <a href="/2026/04/01/meu-cockpit-de-sim-racing-formula-fx1/">cockpit decente de sim racing</a>. Nele eu uso <a href="https://www.emudeck.com/"target="_blank" rel="noopener">EmuDeck</a>, que nasceu justamente pra automatizar instalação e configuração de emuladores no Steam Deck, depois passou a suportar Windows também, e ajuda bastante a reduzir a complicação de montar esse tipo de ambiente. Mas meu desktop principal também é forte demais pra ficar subutilizado. Ele tem uma RTX 5090, e hoje eu uso essa GPU principalmente pra testar LLMs e benchmarking, como contei em <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">Testando LLMs Open Source e Comerciais</a>. Seria um desperdício não usar isso pra gaming também.</p>
<p>Só que no Linux eu queria outra coisa. Eu não queria uma caixa-preta fazendo tudo por mim sem eu saber exatamente o que estava sendo alterado, principalmente no meu PC principal, que é minha máquina de trabalho. Eu queria um setup meu, feito &ldquo;na mão&rdquo; no sentido certo da expressão: não clicar GUI por GUI, mas entender o que está sendo configurado, manter os arquivos sob meu controle, e conseguir reconstruir tudo do jeito que eu gosto. Eu sei que o pessoal do NixOS vai pular aqui pra dizer &ldquo;é só usar Nix&rdquo; — explico mais abaixo por que descartei essa rota. Também não queria transformar a minha máquina de trabalho num carnaval de pacote de emulador, tema GTK, wrapper, launcher, runtime esquisito e config enterrada em <code>~/.config</code>. Nem queria dual boot ou VM. Em 2026, a melhor resposta pra isso, na minha opinião, é <a href="https://distrobox.it/"target="_blank" rel="noopener">Distrobox</a>.</p>
<p>Pelo que o próprio projeto explica, Distrobox é um wrapper em cima de <code>podman</code>, <code>docker</code> ou <code>lilipod</code> pra criar containers fortemente integrados ao host, com acesso a <code>HOME</code>, Wayland/X11, áudio, dispositivos USB, storage externo e GPU. Ou seja: exatamente o tipo de isolamento pragmático que eu queria. Não é uma fronteira de segurança, e o próprio site deixa isso claro. Não use pensando em sandbox de alta segurança. Use pensando em separar ambientes sem ficar pagando o preço de uma VM.</p>
<p>E antes que alguém repita a confusão de sempre: container não é máquina virtual. Eu explico isso com calma neste outro episódio:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/85k8se4Zo70"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>O setup<span class="hx:absolute hx:-mt-20" id="o-setup"></span>
    <a href="#o-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A ideia é simples: um Arch Linux vanilla dentro de um Distrobox chamado <code>gaming</code>, com os caminhos do NAS montados em read-only, a biblioteca Steam acessível onde faz sentido, e todos os emuladores instalados lá dentro. Como o container tem acesso a GPU NVIDIA, áudio, USB e demais periféricos, eu separo trabalho de jogo na mesma máquina sem penalidade prática de performance.</p>
<p>O bootstrap inicial é mais ou menos isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox create --name gaming --image archlinux:latest --nvidia <span class="se">\
</span></span></span><span class="line"><span class="cl">  --home /mnt/data/distrobox/gaming <span class="se">\
</span></span></span><span class="line"><span class="cl">  --volume /mnt/data/steam:/mnt/data/steam <span class="se">\
</span></span></span><span class="line"><span class="cl">  --volume /mnt/terachad/Emulators:/mnt/terachad/Emulators:ro</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Só esse começo já tem pegadinha. O <code>archlinux:latest</code> dentro de Distrobox vem sem <code>[multilib]</code> no <code>pacman.conf</code>, com um <code>sudoers</code> mal configurado, e com o velho problema do <code>--nvidia</code>: as libs do driver do host entram bind-mounted read-only, então qualquer instalação que dependa de <code>nvidia-utils</code> quebra com conflito de arquivo. Só isso já é o tipo de dor de cabeça que, alguns anos atrás, me faria abrir meia dúzia de abas, mais uma penca de terminais, e gastar uma noite inteira em tentativa e erro.</p>
<p>Desta vez eu fiz diferente.</p>
<h2>Claude Code como assistente de infraestrutura<span class="hx:absolute hx:-mt-20" id="claude-code-como-assistente-de-infraestrutura"></span>
    <a href="#claude-code-como-assistente-de-infraestrutura" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu venho ficando cada vez mais confortável em usar Claude Code como meu assistente de infraestrutura nas minhas máquinas pessoais. Eu não faria isso cegamente em servidor de cliente. Mas em máquina minha, em ambiente que eu posso destruir e reconstruir quantas vezes eu quiser, faz todo sentido. Escrevi sobre isso no artigo <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">Migrando meu Home Server com Claude Code</a>, quando usei Claude pra instalar e configurar openSUSE MicroOS, Docker, NFS, firewall, hardening e o resto todo sem eu precisar ficar lembrando comando chato de shell.</p>
<p>Então pensei: por que não fazer a mesma coisa aqui?</p>
<p>Foi exatamente o que eu fiz. Comecei com prompts bem objetivos, sempre pedindo duas coisas ao mesmo tempo: fazer o trabalho e documentar o suficiente pra eu poder reconstruir tudo depois. O histórico mais importante ficou em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/distrobox-gaming-prompts.md"target="_blank" rel="noopener"><code>docs/distrobox-gaming-prompts.md</code></a>.</p>
<p>Vale um esclarecimento: aqueles prompts no GitHub não são as minhas mensagens cruas, do jeito que saíram na hora. Depois que tudo funcionou, eu pedi pro próprio Claude reescrever os prompts de forma organizada e detalhada, só pra fins de documentação. Os prompts originais eram bem mais simples e bem menos específicos. Eu descrevia o objetivo e deixava o Claude descobrir sozinho paths, arquivos de configuração, formatos e comandos necessários.</p>
<p>O primeiro prompt criou a box com <code>--nvidia</code>, <code>--home</code> separado e os mounts corretos. O segundo resolveu os três problemas clássicos do Arch dentro de Distrobox: <code>sudo</code>, <code>multilib</code> e o dummy package de <code>nvidia-utils</code>. O terceiro instalou a base de gaming, inclusive um detalhe que eu teria esquecido fácil se estivesse fazendo na mão: <code>pipewire-pulse</code>, necessário pra vários emuladores não ficarem mudos nem perderem sincronia.</p>
<p>O ponto não é o texto em si. O ponto é o método. Eu não preciso lembrar a ordem exata, nem as opções precisas, nem onde está a documentação de cada detalhe. Eu descrevo o objetivo e deixo o agente carregar o piano. Fico no papel de tech lead: observando, revisando, corrigindo direção quando precisa, e mandando continuar.</p>
<h2>O projeto no GitHub<span class="hx:absolute hx:-mt-20" id="o-projeto-no-github"></span>
    <a href="#o-projeto-no-github" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O setup vive em <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">akitaonrails/distrobox-gaming</a>, estruturado como um projeto Ansible. 17 roles cobrem desde criação da box, bootstrap de pacotes, seed de configs, instalação de DLC, configuração de controle, até verificação pós-setup. Os playbooks principais são:</p>
<ul>
<li><code>site.yml</code>: setup completo do zero</li>
<li><code>reset-configs.yml</code>: reset só das configs (útil quando você quer zerar tuning e redeployar)</li>
<li><code>backup.yml</code> / <code>restore.yml</code>: snapshot e restauração do container inteiro via <code>podman commit</code></li>
<li><code>refresh-shadps4.yml</code>: atualização standalone do shadPS4</li>
<li><code>install-xenia.yml</code>: instalação opcional do Xenia Manager (Wine prefix)</li>
</ul>
<p>O passo a passo completo está em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/rebuild-runbook.md"target="_blank" rel="noopener"><code>docs/rebuild-runbook.md</code></a>. As decisões de pacote (por que AUR em vez de Flatpak, por exemplo) estão em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/distrobox-gaming-packages.md"target="_blank" rel="noopener"><code>docs/distrobox-gaming-packages.md</code></a>.</p>
<h3>Por que não Nix?<span class="hx:absolute hx:-mt-20" id="por-que-não-nix"></span>
    <a href="#por-que-n%c3%a3o-nix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Alguém vai perguntar. &ldquo;Você tá recriando ambiente reproduzível. Por que não usar NixOS, ou pelo menos <code>nix-shell</code>/flakes?&rdquo;</p>
<p>Eu considerei e descartei. O ecossistema de emuladores que eu preciso vive no AUR. Pacotes <code>-bin</code> do AUR são wrappers de AppImage que empacotam o binário oficial do upstream sem compilar nada. <code>rpcs3-bin</code>, <code>pcsx2-latest-bin</code>, <code>eden-bin</code>, <code>duckstation-qt-bin</code>, <code>shadps4-bin</code> — esses binários acompanham o upstream praticamente no dia do lançamento. No Nixpkgs, muitos desses emuladores ficam semanas ou meses atrás do release, e alguns nem existem. Criar um overlay Nix pra cada emulador que eu preciso, manter, e lidar com os patches de compatibilidade quando upstream muda, é trabalho que eu não quero ter.</p>
<p>Tem também o modelo mental. Eu não quero aprender a linguagem Nix, o sistema de derivações e o modelo de avaliação pra montar um ambiente de games pessoal. Ansible eu já conheço, as roles são pastas com tasks em YAML, e o debug é <code>ansible-playbook -vvv</code>. Quando dá errado, eu leio a mensagem de erro, abro o task que falhou, e sei exatamente onde mexer. Se eu precisasse do mesmo nível de reprodutibilidade em escala, num cluster, com rollback atômico, aí sim Nix teria argumento. Pra um distrobox com emuladores na minha máquina pessoal, Ansible resolve com muito menos atrito.</p>
<p>Flatpak também foi testado e descartado. O <code>bwrap</code> (bubblewrap) do Flatpak não aninha direito dentro das mount namespaces do Docker, então apps Flatpak simplesmente não rodam dentro de distrobox. A alternativa seria instalar Flatpak no host, mas aí você volta ao problema de poluir a máquina de trabalho com pacotes de gaming. Além disso, as versões Flatpak de emuladores ficam bem atrás do AUR — o <code>pcsx2-latest-bin</code> do AUR tracka o AppImage 2.7.x, enquanto o Flathub ainda distribui a v2.6.3.</p>
<h2>Plataformas cobertas<span class="hx:absolute hx:-mt-20" id="plataformas-cobertas"></span>
    <a href="#plataformas-cobertas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O setup cobre 12 plataformas hoje:</p>
<table>
  <thead>
      <tr>
          <th>Plataforma</th>
          <th>Emulador</th>
          <th>Renderer</th>
          <th>Destaque</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PS1</td>
          <td>DuckStation</td>
          <td>Vulkan, 8x upscale</td>
          <td>JINC2 filter, PGXP, widescreen hack</td>
      </tr>
      <tr>
          <td>PS2</td>
          <td>PCSX2 2.7.x</td>
          <td>Vulkan, 4x SSAA</td>
          <td>FXAA + PCRTC antiblur, Xbox bindings</td>
      </tr>
      <tr>
          <td>PS3</td>
          <td>RPCS3</td>
          <td>Vulkan</td>
          <td>Per-game configs curados via API</td>
      </tr>
      <tr>
          <td>PS4</td>
          <td>shadPS4</td>
          <td>Vulkan</td>
          <td>Driveclub em foco (experimental)</td>
      </tr>
      <tr>
          <td>PS Vita</td>
          <td>Vita3K</td>
          <td>—</td>
          <td>Seeding padrão</td>
      </tr>
      <tr>
          <td>GameCube</td>
          <td>Dolphin</td>
          <td>Vulkan</td>
          <td>Perfis 8BitDo Ultimate 2</td>
      </tr>
      <tr>
          <td>Wii</td>
          <td>Dolphin</td>
          <td>Vulkan</td>
          <td>Classic Controller + Nunchuk</td>
      </tr>
      <tr>
          <td>Wii U</td>
          <td>Cemu</td>
          <td>Vulkan</td>
          <td>Baseline seed only-if-missing</td>
      </tr>
      <tr>
          <td>Switch</td>
          <td>Eden</td>
          <td>Vulkan</td>
          <td>DLC + cheats Atmosphere</td>
      </tr>
      <tr>
          <td>Dreamcast</td>
          <td>Flycast</td>
          <td>Vulkan, 2880p</td>
          <td>Wrapper hires, widescreen</td>
      </tr>
      <tr>
          <td>OG Xbox</td>
          <td>xemu</td>
          <td>—</td>
          <td>BIOS/HDD via symlink</td>
      </tr>
      <tr>
          <td>Xbox 360</td>
          <td>Xenia (Wine)</td>
          <td>Vulkan</td>
          <td>Project Forza Plus, PGR3/4</td>
      </tr>
  </tbody>
</table>
<p>RetroArch entra por cima pra cobrir o resto (NES, SNES, Genesis, N64, arcade, etc.), com 21 cores do buildbot e 8 asset packs (databases, shaders, cheats, overlays, autoconfig).</p>
<p>Frontend é <a href="https://es-de.org/"target="_blank" rel="noopener">ES-DE</a>. As definições dos sistemas customizados ficam em <code>ansible/group_vars/all/esde.yml</code> e cada emulador entra com um wrapper que já sobe com as variáveis certas de GPU (voltarei nisso).</p>
<h2>GPU hardforce pra NVIDIA<span class="hx:absolute hx:-mt-20" id="gpu-hardforce-pra-nvidia"></span>
    <a href="#gpu-hardforce-pra-nvidia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Num sistema híbrido com NVIDIA dGPU + AMD iGPU (meu caso no desktop), Vulkan às vezes escolhe o iGPU sozinho e o resultado é emulador rodando a 15 fps enquanto a 5090 fica parada. A solução é forçar o ICD Vulkan da NVIDIA em todos os launchers.</p>
<p>Cada entrada de desktop rendereizada pelo Ansible exporta <code>VK_ICD_FILENAMES</code> apontando pro ICD da NVIDIA, e o wrapper do ES-DE faz a mesma coisa antes de lançar qualquer core. Em combinação com a flag <code>--nvidia</code> na criação da box, que bind-monta os drivers do host em read-only, o resultado é previsível: Vulkan sempre na dGPU NVIDIA, independente do estado do sistema na hora.</p>
<p>Isso vale principalmente pra quem tem setup híbrido. Num desktop com só uma GPU, o ICD filtering é placebo, mas também não atrapalha.</p>
<h2>PS2: PCSX2 com tuning por jogo<span class="hx:absolute hx:-mt-20" id="ps2-pcsx2-com-tuning-por-jogo"></span>
    <a href="#ps2-pcsx2-com-tuning-por-jogo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A linha Gran Turismo é meu vício declarado. Cada versão precisa de tweak específico. O setup do PCSX2 hoje tem configs globais razoáveis (Vulkan, 4x SSAA, widescreen 16:9, Xbox-style bindings, FXAA + PCRTC antiblur, 16x anisotropic, deinterlace Automatic) e per-game INIs pros títulos que eu realmente jogo:</p>
<ul>
<li><strong>Gran Turismo 3 A-spec</strong> (SCUS-97102 + bundle PBPX-95503): widescreen pnach, pack de retextura opcional (desabilitado por default porque NFS causa flicker em cutscenes de showcase de carro)</li>
<li><strong>Gran Turismo 4 USA</strong> (SCUS-97328): HD HUD do Silentwarior112, pack completo de retextura, patches do Silent pra gatilho e câmera, widescreen pnach</li>
<li><strong>Gran Turismo 4 Spec II</strong> (SCUS-97436, CRC <code>4CE521F2</code>): caso especial — os HD packs de GT4 vanilla são linkados via symlink porque o Spec II tem mesma estrutura mas CRC diferente. Widescreen pnach renomeado pra bater com o CRC. Deinterlace mode 8 (Adaptive TFF), ShadeBoost tuning (+10 saturação, +3 brilho, +2 contraste) pra corrigir ghosting de pause-interlace e saturação anêmica do upscale</li>
<li><strong>Enthusia Professional Racing</strong> (SLUS-20967): HD textures + widescreen</li>
<li><strong>Ridge Racer V</strong> (SLUS-20002): widescreen + no-interlace pnach</li>
</ul>
<p>Os pnach files do Silent e da comunidade são baixados automaticamente pelo role <code>pcsx2_textures</code>, que também faz o deploy do pack de cheats do NAS e instala os HD packs do Spec II via symlink. Widescreen é ligado via <code>gamesettings</code> por CRC (PCSX2 identifica jogo por CRC antes de aplicar o pnach).</p>
<p>Sobre versão do PCSX2: o <code>pcsx2-latest-bin</code> do AUR tracka a linha 2.7.x, que é onde todas as features modernas estão (texture replacement, FXAA, PCRTC antiblur). O <code>pcsx2</code> padrão dos repos oficiais do Arch ainda está na 2.6.3. A diferença é mais de 250 commits de correção e feature novo.</p>
<h2>PS3: RPCS3 com per-game configs curados<span class="hx:absolute hx:-mt-20" id="ps3-rpcs3-com-per-game-configs-curados"></span>
    <a href="#ps3-rpcs3-com-per-game-configs-curados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>RPCS3 tem um detalhe chato: configs por jogo ficam em <code>~/.config/rpcs3/custom_configs/config_&lt;SERIAL&gt;.yml</code>. O formato do arquivo tem versão, e se você gerar um YAML manualmente com versão errada, o RPCS3 rejeita silenciosamente e volta pro default. Isso custou tempo pra descobrir.</p>
<p>A role <code>rpcs3_per_game_configs</code> resolve isso assim:</p>
<ol>
<li>Query o <a href="https://rpcs3.net/compatibility"target="_blank" rel="noopener">compatibility DB da comunidade RPCS3</a> pra pegar os presets recomendados por título</li>
<li>Aplica overrides curados por cima pra títulos que eu sei que precisam de ajuste específico (tipicamente GT5 e GT6)</li>
<li>Salva o YAML com a filename convention exata (<code>config_&lt;SERIAL&gt;.yml</code>, prefixo <code>config_</code> obrigatório)</li>
</ol>
<p>Na linha Gran Turismo:</p>
<ul>
<li><strong>GT5</strong> (BCUS98114, BCES00569): Resolution Scale 300%, Shader Precision Ultra, Force High Precision Z, SPU XFloat Accurate, Multithreaded RSX. Combinação que mata o dithering típico do RSX sem quebrar a GT series safety config (WCB/RCB off).</li>
<li><strong>GT6</strong> (BCES01893 + variantes regionais): caso especial, <strong>versão travada em v1.05</strong>. Patches 1.06+ regridem com superfícies pretas em carros no menu garagem. O script <code>extract_ps3_dlc.py</code> tem per-title PATCH ceiling justamente pra pinar o GT6 no 1.05 mesmo quando o PSN cuspe patches mais novos. Além disso, Force CPU Blit ligado é mandatório (sem ele, flicker de tela inteira). O trade-off é que o retrovisor fica permanentemente preto — ligar WCB restauraria o retrovisor mas downgradaria pra 720p nativo. Prefiro perder o retrovisor.</li>
</ul>
<p>Mais detalhes desses dois em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/gt5-rpcs3.md"target="_blank" rel="noopener"><code>docs/gt5-rpcs3.md</code></a> e <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/gt6-rpcs3.md"target="_blank" rel="noopener"><code>docs/gt6-rpcs3.md</code></a>.</p>
<h3>Update checker de PS3<span class="hx:absolute hx:-mt-20" id="update-checker-de-ps3"></span>
    <a href="#update-checker-de-ps3" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra não ter que abrir a GUI do RPCS3 e ficar clicando em <code>Download Game Updates</code> jogo por jogo, criei um script em Python: <code>check_ps3_updates.py</code>. Ele percorre os jogos instalados em <code>dev_hdd0/game</code>, parseia o <code>PARAM.SFO</code> pra pegar a versão atual de cada título, consulta o servidor de updates da PSN (<code>a0.ww.np.dl.playstation.net</code>) e compara com o cache local de patches. <code>--list</code> mostra o diff; <code>--download</code> baixa o que falta. <code>--max-version</code> respeita os teto por título (ex: 1.05 pro GT6).</p>
<p>O primeiro scan na minha biblioteca achou 51 de 72 jogos com update disponível, 69 patches faltando localmente, uns 24 GB no total. Só o GT6 sozinho tinha 16 patches faltando, da 1.07 até 1.22, que eu descartei pelos motivos acima.</p>
<p>Os patches chegam como pacotes PSN tipo 0x0001 sem criptografia, então o próprio <code>extract_ps3_dlc.py</code> consegue instalar depois. A chamada padrão:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox enter gaming -- python3 scripts/extract_ps3_dlc.py <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;/mnt/terachad/Emulators/EmuDeck/roms_heavy/ps3-DLC&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dest <span class="s2">&#34;/mnt/data/distrobox/gaming/.config/rpcs3/dev_hdd0/game&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Acompanhar versão de DLC de PS3 manualmente, jogo por jogo no RPCS3, é inviável. Vira lista de tarefas eterna e você só descobre que tinha patch 1.22 quando o jogo crasha.</p>
<h2>PS1: DuckStation com per-game<span class="hx:absolute hx:-mt-20" id="ps1-duckstation-com-per-game"></span>
    <a href="#ps1-duckstation-com-per-game" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A trilogia Gran Turismo começa no PS1, e a qualidade ali só sobe se você ligar as coisas certas:</p>
<ul>
<li><strong>Gran Turismo</strong> (SCUS-94949): built-in WidescreenHack do DuckStation (não existe cheat pra esse jogo), PGXP ligado</li>
<li><strong>Gran Turismo 2 Simulation</strong> (SCUS-94455): widescreen cheat + 8 MB RAM cheat (fix de áudio em track crowded), filter JINC2</li>
<li><strong>Gran Turismo 2 Arcade</strong> (SCUS-94488): mesmo tratamento do Simulation</li>
</ul>
<p>Global defaults: Vulkan, 8x upscale, JINC2 como filter default.</p>
<p>Os cheats pro GT2 saem de um repositório de widescreen fixes e são ligados automaticamente na pasta de cheats do DuckStation.</p>
<h2>Switch: Eden com DLC, update e cheats<span class="hx:absolute hx:-mt-20" id="switch-eden-com-dlc-update-e-cheats"></span>
    <a href="#switch-eden-com-dlc-update-e-cheats" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Switch, <code>eden-bin</code> é o emulador. A integração automatiza três coisas que antes eu fazia na mão:</p>
<ol>
<li>
<p><strong>Install de DLC e update</strong>: o role <code>install_dlcs</code> aponta pro diretório de dumps no NAS, identifica os arquivos <code>.nsp</code>/<code>.nsz</code>/<code>.xci</code>/<code>.xcz</code>, e instala tudo em <code>~/.local/share/eden/nand/user/Contents/registered/</code>. Se o dump está bagunçado (patches e DLC misturados em flat folder), o script <code>reorganize_switch_nsps.py</code> sorteia tudo por Title ID antes.</p>
</li>
<li>
<p><strong>Cheats Atmosphere</strong>: os cheats em formato Atmosphere vão symlinkados pro load path do Eden (<code>~/.local/share/eden/load/&lt;TITLE_ID&gt;/cheats/</code>). O role <code>switch_cheats</code> faz isso por todos os títulos cobertos pelo pack do NAS.</p>
</li>
<li>
<p><strong>Update checker</strong>: <code>check_switch_updates.py</code> lista títulos com update disponível vs o que tá instalado localmente. Útil pra não perder patch importante de jogo que sai atualização ainda.</p>
</li>
</ol>
<p>Atenção ao detalhe que me queimou: <code>QT_STYLE_OVERRIDE</code> precisa ser desarmado antes de lançar o <code>eden-bin</code>, senão ele conflita com Kvantum e quebra a UI. O wrapper já faz o <code>unset</code> antes do <code>exec</code>.</p>
<h2>Xbox 360: Xenia Manager via Wine<span class="hx:absolute hx:-mt-20" id="xbox-360-xenia-manager-via-wine"></span>
    <a href="#xbox-360-xenia-manager-via-wine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Xbox 360 ainda é território áspero no Linux. Não tem emulador nativo decente. A melhor rota hoje é <a href="https://xenia.jp/"target="_blank" rel="noopener">Xenia</a> rodando via Wine, gerenciado pelo <a href="https://github.com/xenia-manager/xenia-manager"target="_blank" rel="noopener">Xenia Manager</a>.</p>
<p>Isso é opt-in no meu setup. O playbook <code>install-xenia.yml</code> cria um Wine prefix dedicado, baixa o Xenia Manager, e registra os launchers. Não faz parte do <code>site.yml</code> principal porque nem todo mundo quer Wine no meio do caminho.</p>
<h3>Project Forza Plus (Forza Motorsport 2/3/4)<span class="hx:absolute hx:-mt-20" id="project-forza-plus-forza-motorsport-234"></span>
    <a href="#project-forza-plus-forza-motorsport-234" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Forza antigo ainda é o rei do simcade de geração passada. Rodar FM2/3/4 no Xenia requer mods da comunidade Project Forza Plus: patches de compatibilidade, mods de performance, instalação de title updates específicos. Documentei o processo em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/project-forza.md"target="_blank" rel="noopener"><code>docs/project-forza.md</code></a>.</p>
<h3>Project Gotham Racing 3 e 4<span class="hx:absolute hx:-mt-20" id="project-gotham-racing-3-e-4"></span>
    <a href="#project-gotham-racing-3-e-4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>PGR3 roda razoavelmente bem no Xenia Canary. PGR4 precisa de ajuste específico: <code>render_target_path_vulkan = &quot;fsi&quot;</code> no config, senão algumas races quebram com artefato visual. Documentei o setup em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/xbox360-pgr.md"target="_blank" rel="noopener"><code>docs/xbox360-pgr.md</code></a>. Áudio do PGR4 no NVIDIA ainda tem problema de XMA decoding que gera garbage intermitente. Tem issue aberta no xenia-canary, sem resolução up até agora.</p>
<h3>Title Updates em batch<span class="hx:absolute hx:-mt-20" id="title-updates-em-batch"></span>
    <a href="#title-updates-em-batch" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Title Update pra jogo de Xbox 360 era distribuído pela Xbox Live, que já foi desligada. A alternativa é o archive.org, que tem o catálogo completo preservado. Escrevi <code>scripts/download-xbox360-tus.py</code> pra automatizar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox enter gaming -- python3 scripts/download-xbox360-tus.py <span class="se">\
</span></span></span><span class="line"><span class="cl">  --src /mnt/terachad/Emulators/EmuDeck/roms_heavy/xbox360 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dest /mnt/terachad/Emulators/EmuDeck/roms_heavy/xbox360-updates <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dry-run</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O script precisa do CLI <code>internetarchive</code> autenticado (<code>ia configure</code> uma vez, guarda o token). Ele scaneia a pasta de jogos, faz match contra o manifest do archive.org (cacheado localmente), prioriza por região (USA &gt; World &gt; Europe &gt; Japan), e baixa via <code>ia download --checksum</code> com retry automático. Os .zip resultantes vão pro Xenia Manager via <code>Manage → Install Content</code>, que extrai pro diretório correto (<code>000B0000/</code>).</p>
<p>O setup completo tá em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/xbox360-title-updates.md"target="_blank" rel="noopener"><code>docs/xbox360-title-updates.md</code></a>.</p>
<h2>Steam no distrobox<span class="hx:absolute hx:-mt-20" id="steam-no-distrobox"></span>
    <a href="#steam-no-distrobox" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Desde que o Proton virou cidadão de primeira no Linux, faz sentido ter Steam também no gaming box. Adicionei o pacote <code>steam</code> no Ansible (que puxa multilib e dependências 32-bit), montei <code>/mnt/data/steam</code> em read-write pra a biblioteca persistir fora do container, e criei launcher no host.</p>
<p>Na prática, isso me permite rodar jogos Steam via Proton lado a lado com emulação, sem precisar alternar ambiente. O mini-PC com RTX 4090 continua sendo a máquina principal de Steam no cockpit de sim racing. O desktop é fallback e ambiente de teste.</p>
<h2>Shell e controle por controle<span class="hx:absolute hx:-mt-20" id="shell-e-controle-por-controle"></span>
    <a href="#shell-e-controle-por-controle" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Shell mínimo dentro da box<span class="hx:absolute hx:-mt-20" id="shell-mínimo-dentro-da-box"></span>
    <a href="#shell-m%c3%adnimo-dentro-da-box" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando eu entro no gaming box via <code>distrobox enter gaming</code>, eu quero prompt decente mesmo pra troubleshooting rápido. Instalei <code>zsh</code> + <code>starship</code> com config mínima, zero plugins exóticos. Não é meu setup de dev full, é só &ldquo;prompt que mostra path e git branch sem me dar vergonha&rdquo;.</p>
<h3>Dolphin e o inferno do controle moderno<span class="hx:absolute hx:-mt-20" id="dolphin-e-o-inferno-do-controle-moderno"></span>
    <a href="#dolphin-e-o-inferno-do-controle-moderno" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>Dolphin</code> sempre foi o rei do atrito manual. Se você usa um controle mais moderno, como meu 8BitDo Ultimate 2, precisa lembrar como eu gosto do mapeamento de GameCube, como adapto o Wii Remote, quando usar perfil de Nunchuk, quando trocar pro Classic Controller. Eu não tenho a menor paciência pra reconstruir isso na mão toda vez. Os perfis ficam prontos em <code>config/emulator-overrides/dolphin/Profiles/</code> e são copiados pelo role <code>seed_configs</code>. Detalhes em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/controller-hotkeys.md"target="_blank" rel="noopener"><code>docs/controller-hotkeys.md</code></a>.</p>
<h2>Alguns tropeços que sobraram<span class="hx:absolute hx:-mt-20" id="alguns-tropeços-que-sobraram"></span>
    <a href="#alguns-trope%c3%a7os-que-sobraram" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nem tudo virou magia. Emulação sempre tem casca de banana.</p>
<p><code>Flycast</code>, por exemplo, tem uma armadilha irritante. O arquivo <code>emu.cfg</code> usa chaves <code>rend.*</code> dentro da seção <code>[config]</code>. Se você cria uma seção <code>[rend]</code> separada, parece certo, mas o emulador simplesmente ignora e depois reescreve tudo com default medíocre. A correção virou wrapper dedicado em <code>$DG_BOX_HOME/bin/flycast-hires</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">$DG_BOX_HOME</span>/bin/flycast-hires <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:pvr.rend<span class="o">=</span><span class="m">4</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.Resolution<span class="o">=</span><span class="m">2880</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.EmulateFramebuffer<span class="o">=</span>no <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.WideScreen<span class="o">=</span>yes</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhes em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/flycast-resolution.md"target="_blank" rel="noopener"><code>docs/flycast-resolution.md</code></a>.</p>
<p><code>shadPS4</code> ainda está longe de ser um caso encerrado. O setup atual é focado em <code>Driveclub</code>, com config espelhada, patch XML e links dos <code>sys_modules</code> do firmware 11.00. E isso não foi por acaso: eu gosto muito de <code>Driveclub</code>. Aliás, é praticamente o único jogo ainda preso no PS4 que eu realmente queria conseguir rodar direito. Eu já vi vários vídeos no YouTube mostrando o jogo funcionando, mas até agora eu mesmo não consegui fazer isso rodar de forma satisfatória no Linux. Documentado em <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/driveclub-shadps4.md"target="_blank" rel="noopener"><code>docs/driveclub-shadps4.md</code></a>. A parte boa é que agora eu não dependo mais de memória muscular pra lembrar como lançar, com qual patch, em que pasta, com quais módulos. A parte ruim é que emulação de PS4 ainda é emulação de PS4. Não existe script que faça upstream amadurecer mais rápido. Se alguém souber como fechar essa configuração no Linux, olhe o projeto no GitHub e mande um PR.</p>
<p>Ridge Racer V tem um flickering de textura de carro documentado nas issues do PCSX2 (#3639, #13729). O renderer Hardware é aceitável pra jogo de corrida onde você não fica admirando parado o modelo. Software renderer resolve mas mata performance. Escolhi aceitar o flicker.</p>
<h2>O ganho real não é só automação<span class="hx:absolute hx:-mt-20" id="o-ganho-real-não-é-só-automação"></span>
    <a href="#o-ganho-real-n%c3%a3o-%c3%a9-s%c3%b3-automa%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seria fácil resumir isso como &ldquo;olha que legal, usei IA pra automatizar shell script&rdquo;. Não é isso.</p>
<p>O ganho real é mais chato e mais importante: eu parei de gastar energia mental com trabalho braçal. Não precisei mais lembrar comando raro. Não precisei manter dezenas de abas com wiki, issue, fórum e README. Não precisei deixar meia dúzia de terminais espalhados dando <code>tail</code> em log enquanto tento lembrar que opção escondida de GUI o emulador insiste em regravar no primeiro boot. Eu passei a trabalhar em conversa.</p>
<p>Eu digo pro agente o objetivo. Ele procura os arquivos, lê os logs, encontra o formato certo do config, compara defaults, propõe correções, escreve script, valida paths, verifica UID/GID, confere symlink quebrado, gera wrapper, exporta launcher. Eu continuo responsável pelas decisões, claro. Mas parei de ser digitador de comando raro.</p>
<p>Esse é o ponto que mais me interessa no uso de coding agents em Linux. Eles reduzem dramaticamente a fricção de entrada. Muita gente desiste do desktop Linux não porque o sistema seja incapaz, mas porque a curva de tuning historicamente foi irritante demais. Ter um assistente capaz de ler docs, cruzar config, propor automação e executar com supervisão muda esse jogo.</p>
<h2>Se você quiser reproduzir<span class="hx:absolute hx:-mt-20" id="se-você-quiser-reproduzir"></span>
    <a href="#se-voc%c3%aa-quiser-reproduzir" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O repo foi publicado pra isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/akitaonrails/distrobox-gaming.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> distrobox-gaming/ansible
</span></span><span class="line"><span class="cl">ansible-galaxy collection install -r collections/requirements.yml
</span></span><span class="line"><span class="cl">cp host_vars/localhost.yml.example host_vars/localhost.yml
</span></span><span class="line"><span class="cl"><span class="nv">$EDITOR</span> host_vars/localhost.yml  <span class="c1"># ajuste paths da sua máquina</span>
</span></span><span class="line"><span class="cl">ansible-playbook site.yml</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Xenia é opt-in:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ansible-playbook install-xenia.yml</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Leia o <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/README.md"target="_blank" rel="noopener"><code>README.md</code></a> e o <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/rebuild-runbook.md"target="_blank" rel="noopener"><code>docs/rebuild-runbook.md</code></a> antes. Eu não distribuo ROM, BIOS, firmware nem chaves. O repo só detecta, linka e configura o que você já tem na sua própria máquina.</p>
<h2>Se você preferir a rota com Claude Code<span class="hx:absolute hx:-mt-20" id="se-você-preferir-a-rota-com-claude-code"></span>
    <a href="#se-voc%c3%aa-preferir-a-rota-com-claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Minha recomendação é simples:</p>
<ol>
<li>Comece pelo objetivo, não pelo comando. &ldquo;Quero um distrobox Arch com GPU NVIDIA, home separado, ROMs read-only e Steam rw&rdquo; é melhor do que despejar meia linha de shell sem contexto.</li>
<li>Peça sempre pra documentar o que está fazendo. Se der certo, promova o resultado a script ou role. Se não documentar, você acabou de fabricar uma gambiarra descartável.</li>
<li>Trabalhe em fases. Criação da box. Bootstrap. Config de emuladores. Export de launchers. Verificação. Foi exatamente assim que eu quebrei o problema.</li>
<li>Peça verificações objetivas. <code>command -v</code>, existência de arquivos, symlinks quebrados, UID/GID, path de ROM, áudio, GPU. A melhor automação é a que falha cedo.</li>
<li>Não use agente como papagaio de comando. Use como assistente técnico. Você continua revisando as decisões e mandando ajustar o rumo.</li>
</ol>
<p>Pra mim, esse foi o ganho. Eu saí do zero pra uma máquina de emulação muito mais completa sem precisar customizar tudo na unha, em GUI, uma janela por vez. E, dessa vez, terminei com energia pra fazer o que eu queria desde o começo.</p>
<p>Jogar.</p>
]]></content:encoded><category>emulacao</category><category>containers</category><category>automacao</category></item><item><title>Discord como Admin Panel | Bastidores do The M.Akita Chronicles</title><link>https://akitaonrails.github.io/2026/02/20/discord-como-admin-panel-bastidores-do-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/20/discord-como-admin-panel-bastidores-do-the-m-akita-chronicles/</guid><pubDate>Fri, 20 Feb 2026 03:18:24 +0000</pubDate><description>&lt;p&gt;Este post vai fazer parte de uma série; acompanhe pela tag &lt;a href="https://akitaonrails.github.io/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. Esta é a parte 6.&lt;/p&gt;
&lt;p&gt;E não deixe de assinar minha nova newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Todo projeto Rails começa igual: você cria um admin panel. Active Admin, Administrate ou um CRUD caseiro. Tabelas, formulários, botões. E a cada semana, você usa 5% das telas que construiu.&lt;/p&gt;
&lt;p&gt;Agora imagine que seu admin panel é uma conversa. Você cola um link; o sistema processa. Digita &lt;code&gt;/week&lt;/code&gt; e vê o resumo. &lt;code&gt;/approve&lt;/code&gt; e a newsletter está agendada. &lt;code&gt;/ask akita como funciona o pipeline de podcast?&lt;/code&gt; e uma IA te explica a arquitetura do seu próprio sistema.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post vai fazer parte de uma série; acompanhe pela tag <a href="/tags/the-m-akita-chronicles/">/themakitachronicles</a>. Esta é a parte 6.</p>
<p>E não deixe de assinar minha nova newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Todo projeto Rails começa igual: você cria um admin panel. Active Admin, Administrate ou um CRUD caseiro. Tabelas, formulários, botões. E a cada semana, você usa 5% das telas que construiu.</p>
<p>Agora imagine que seu admin panel é uma conversa. Você cola um link; o sistema processa. Digita <code>/week</code> e vê o resumo. <code>/approve</code> e a newsletter está agendada. <code>/ask akita como funciona o pipeline de podcast?</code> e uma IA te explica a arquitetura do seu próprio sistema.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-40-53.jpg" alt="/ask akita"  loading="lazy" /></p>
<p>Esse é o Discord como interface operacional. Não é gambiarra — é <strong>a melhor interface CRUD que existe pra um time de uma pessoa</strong>.</p>
<h2>Por Que Discord e Não Um Web Admin?<span class="hx:absolute hx:-mt-20" id="por-que-discord-e-não-um-web-admin"></span>
    <a href="#por-que-discord-e-n%c3%a3o-um-web-admin" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos ser honestos sobre o que um admin panel exige:</p>
<ol>
<li><strong>Autenticação</strong>: login, sessão, CSRF, password reset</li>
<li><strong>Autorização</strong>: roles, permissions, who can do what</li>
<li><strong>UI</strong>: tabelas, paginação, formulários, validação client-side</li>
<li><strong>Deployment</strong>: outra rota, outro controller, outro set de testes</li>
<li><strong>Mobile</strong>: responsive? App? PWA?</li>
</ol>
<p>E o que o Discord já te dá de graça:</p>
<ol>
<li><strong>Auth</strong>: Discord cuida. Você verifica o <code>author_id</code></li>
<li><strong>Autorização</strong>: uma lista de admin IDs e pronto</li>
<li><strong>UI</strong>: embeds com cores, campos, thumbnails, reações como status</li>
<li><strong>Deployment</strong>: zero. Usa o que já está deployado</li>
<li><strong>Mobile</strong>: app nativo no iOS e Android, push notifications</li>
</ol>
<p>O trade-off? Não serve pra tudo. Um CRUD complexo com formulários de 20 campos? Melhor um web admin. Mas pra operações de comando/resposta — que são 90% do que você faz no dia a dia — Discord é imbatível.</p>
<h2>discordrb: A Gem Que Funciona<span class="hx:absolute hx:-mt-20" id="discordrb-a-gem-que-funciona"></span>
    <a href="#discordrb-a-gem-que-funciona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A gem <a href="https://github.com/shardlab/discordrb"target="_blank" rel="noopener"><code>discordrb</code></a> (fork do shardlab, versão 3.7.2) é a mais madura pra Ruby. Não é perfeita — tem idiossincrasias. Mas funciona de verdade em produção.</p>
<p>O setup mínimo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span> <span class="o">=</span> <span class="no">Discordrb</span><span class="o">::</span><span class="no">Bot</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="ss">token</span><span class="p">:</span> <span class="n">token</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">intents</span><span class="p">:</span> <span class="no">Discordrb</span><span class="o">::</span><span class="no">INTENTS</span><span class="o">[</span><span class="ss">:server_messages</span><span class="o">]</span> <span class="o">|</span>
</span></span><span class="line"><span class="cl">           <span class="no">Discordrb</span><span class="o">::</span><span class="no">INTENTS</span><span class="o">[</span><span class="ss">:direct_messages</span><span class="o">]</span> <span class="o">|</span>
</span></span><span class="line"><span class="cl">           <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">15</span><span class="p">)</span>  <span class="c1"># message_content intent</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">message</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Toda mensagem passa por aqui</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">run</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aquele <code>(1 &lt;&lt; 15)</code> é o <em>Message Content Intent</em>. Sem ele, seu bot recebe eventos mas o <code>event.content</code> vem vazio. Precisa habilitar no Discord Developer Portal &gt; Bot &gt; Privileged Gateway Intents. É a primeira armadilha que pega todo mundo.</p>
<p><a href="https://discord.com/developers/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-58-46.jpg" alt="discord dashboard"  loading="lazy" /></a></p>
<p>Não vou detalhar como configurar um bot no Discord, tem <a href="https://www.ionos.com/digitalguide/server/know-how/creating-discord-bot/"target="_blank" rel="noopener">vários tutoriais</a> pra isso já, você acha fácil online. Vou focar só no meu caso de uso como exemplo.</p>
<h2>Padrão 1: Parser + Dispatcher<span class="hx:absolute hx:-mt-20" id="padrão-1-parser--dispatcher"></span>
    <a href="#padr%c3%a3o-1-parser--dispatcher" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Misturar parsing de comandos com lógica de negócio é receita pra espaguete. Separa em três camadas:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Discord Event → Parser → Dispatcher → Job/Service</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O runner (que conversa com a API do Discord) é burro de propósito:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">message</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="k">next</span> <span class="k">if</span> <span class="n">event</span><span class="o">.</span><span class="n">author</span><span class="o">.</span><span class="n">bot_account?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">parsed</span> <span class="o">=</span> <span class="no">SlashCommandParser</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">event</span><span class="o">.</span><span class="n">content</span><span class="p">,</span> <span class="ss">attachments</span><span class="p">:</span> <span class="n">urls</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">parsed</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="no">SlashCommandParser</span><span class="o">::</span><span class="no">Command</span>
</span></span><span class="line"><span class="cl">    <span class="no">CommandDispatcher</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">parsed</span><span class="p">,</span> <span class="ss">channel_id</span><span class="p">:</span> <span class="n">channel_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">react</span><span class="p">(</span><span class="s2">&#34;\u{2705}&#34;</span><span class="p">)</span>  <span class="c1"># checkmark</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="no">SlashCommandParser</span><span class="o">::</span><span class="no">Submission</span>
</span></span><span class="line"><span class="cl">    <span class="no">ProcessMessageJob</span><span class="o">.</span><span class="n">perform_later</span><span class="p">(</span><span class="ss">url</span><span class="p">:</span> <span class="n">parsed</span><span class="o">.</span><span class="n">url</span><span class="p">,</span> <span class="ss">tags</span><span class="p">:</span> <span class="n">parsed</span><span class="o">.</span><span class="n">tags</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">react</span><span class="p">(</span><span class="s2">&#34;\u{1F916}&#34;</span><span class="p">)</span>  <span class="c1"># robot</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">react</span><span class="p">(</span><span class="s2">&#34;\u{274C}&#34;</span><span class="p">)</span>  <span class="c1"># red X</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Três resultados visuais instantâneos: checkmark (comando aceito), robô (URL sendo processada), X vermelho (erro). O usuário sabe o que aconteceu sem esperar nenhuma resposta textual.</p>
<p>O parser retorna objetos tipados, não strings. O dispatcher faz um <code>case</code> gigante mas cada handler é um método isolado. Cada handler chama um job ou service. <strong>Nada de lógica de negócio no runner.</strong></p>
<h3>Evolução: Discord Application Commands<span class="hx:absolute hx:-mt-20" id="evolução-discord-application-commands"></span>
    <a href="#evolu%c3%a7%c3%a3o-discord-application-commands" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A versão inicial parseava texto puro — o usuário digitava <code>/week</code> e o parser interpretava. Funciona, mas tem limitações: sem autocomplete, sem validação de argumentos, sem documentação inline.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-56-15.jpg" alt="help"  loading="lazy" /></p>
<p>O Discord tem um sistema nativo de <strong>Application Commands</strong> — slash commands registrados na API que aparecem com autocomplete, descrição, e tipagem de parâmetros. Quando o usuário digita <code>/</code>, o Discord mostra todos os comandos disponíveis com descrição.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:week</span><span class="p">,</span> <span class="s2">&#34;List this week&#39;s stories&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">guild_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:approve</span><span class="p">,</span> <span class="s2">&#34;Approve newsletter for sending&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">guild_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:ask</span><span class="p">,</span> <span class="s2">&#34;Ask AI a question&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">guild_id</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="s2">&#34;question&#34;</span><span class="p">,</span> <span class="s2">&#34;Your question&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="s2">&#34;personality&#34;</span><span class="p">,</span> <span class="s2">&#34;AI personality&#34;</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&#34;Akita&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;akita&#34;</span><span class="p">,</span> <span class="s2">&#34;Marvin&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;marvin&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O handler usa <code>bot.application_command</code> ao invés de parsear texto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">application_command</span><span class="p">(</span><span class="ss">:week</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">CommandDispatcher</span><span class="o">.</span><span class="n">dispatch_week</span><span class="p">(</span><span class="ss">channel_id</span><span class="p">:</span> <span class="n">event</span><span class="o">.</span><span class="n">channel</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">respond</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;Listing stories...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Os dois sistemas coexistem: Application Commands pra quem prefere autocomplete, texto puro pra quem prefere digitar rápido. O dispatcher é o mesmo — a diferença é só como o comando chega.</p>
<h2>Padrão 2: Reações Como Status<span class="hx:absolute hx:-mt-20" id="padrão-2-reações-como-status"></span>
    <a href="#padr%c3%a3o-2-rea%c3%a7%c3%b5es-como-status" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-43-55.jpg" alt="reactions"  loading="lazy" /></p>
<p>A feature mais subestimada do Discord pra bots é reações. Elas são feedback visual instantâneo que não polui o canal com mensagens:</p>
<ul>
<li><strong>Checkmark</strong> = comando recebido e enfileirado</li>
<li><strong>Robô</strong> = processamento assíncrono iniciado</li>
<li><strong>X vermelho</strong> = erro (detalhes no embed que segue)</li>
</ul>
<p>O usuário cola um link. Vê o robô aparecer. 30 segundos depois, um embed rico aparece com título, resumo, imagem e comentário da IA. Se algo falhar, aparece um embed vermelho com a mensagem de erro.</p>
<p>Isso é melhor que qualquer spinner de loading numa web UI.</p>
<h2>Padrão 3: Embeds Ricos Como Dashboard<span class="hx:absolute hx:-mt-20" id="padrão-3-embeds-ricos-como-dashboard"></span>
    <a href="#padr%c3%a3o-3-embeds-ricos-como-dashboard" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-44-36.jpg" alt="new story"  loading="lazy" /></p>
<p>Discord embeds são surpreendentemente poderosos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">embed</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">title</span><span class="p">:</span> <span class="s2">&#34;Story Preview&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">url</span><span class="p">:</span> <span class="n">source_url</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">color</span><span class="p">:</span> <span class="mh">0xFF4500</span><span class="p">,</span>           <span class="c1"># vermelho-alaranjado pra high priority</span>
</span></span><span class="line"><span class="cl">  <span class="ss">description</span><span class="p">:</span> <span class="n">summary</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">thumbnail</span><span class="p">:</span> <span class="p">{</span> <span class="ss">url</span><span class="p">:</span> <span class="n">image</span> <span class="p">},</span>  <span class="c1"># imagem do artigo</span>
</span></span><span class="line"><span class="cl">  <span class="ss">fields</span><span class="p">:</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;M.Akita diz&#34;</span><span class="p">,</span> <span class="ss">value</span><span class="p">:</span> <span class="n">akita_comment</span><span class="p">,</span> <span class="ss">inline</span><span class="p">:</span> <span class="kp">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;M.Arvin diz&#34;</span><span class="p">,</span> <span class="ss">value</span><span class="p">:</span> <span class="n">marvin_comment</span><span class="p">,</span> <span class="ss">inline</span><span class="p">:</span> <span class="kp">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;File&#34;</span><span class="p">,</span> <span class="ss">value</span><span class="p">:</span> <span class="s2">&#34;`stories/2026-02-16-titulo.md`&#34;</span><span class="p">,</span> <span class="ss">inline</span><span class="p">:</span> <span class="kp">false</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">footer</span><span class="p">:</span> <span class="p">{</span> <span class="ss">text</span><span class="p">:</span> <span class="s2">&#34;fire high | tech_news | verified&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">Discordrb</span><span class="o">::</span><span class="no">API</span><span class="o">::</span><span class="no">Channel</span><span class="o">.</span><span class="n">create_message</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">channel_id</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="o">[</span><span class="n">embed</span><span class="o">]</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cores indicam prioridade (vermelho = high, âmbar = medium, verde = low). O footer mostra score, seção e status de fact-check tudo numa linha. Você bate o olho e sabe o estado da coisa.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-45-11.jpg" alt="week list"  loading="lazy" /></p>
<p>Para relatórios, múltiplos embeds lado a lado (Discord aceita até 10 por mensagem) funcionam como um mini-dashboard. O comando <code>/week</code> lista todas as histórias com ações inline — <code>/load @a3f2b1</code> pra ver detalhes, <code>/delete @a3f2b1</code> pra remover. O hash é estável (baseado no path do arquivo), então não muda quando você deleta outra história.</p>
<h2>Padrão 4: Context Per Channel<span class="hx:absolute hx:-mt-20" id="padrão-4-context-per-channel"></span>
    <a href="#padr%c3%a3o-4-context-per-channel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-46-29.jpg" alt="load"  loading="lazy" /></p>
<p>Quando o usuário digita <code>/load #3</code>, o #3 se refere à listagem mais recente. Mas e se dois canais estão sendo usados simultaneamente?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ChannelContext</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
</span></span><span class="line"><span class="cl">  <span class="no">EXPIRY_DURATION</span> <span class="o">=</span> <span class="mi">2</span><span class="o">.</span><span class="n">hours</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">for_channel</span><span class="p">(</span><span class="n">channel_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">find_or_create_by!</span><span class="p">(</span><span class="ss">channel_id</span><span class="p">:</span> <span class="n">channel_id</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">set_week_stories!</span><span class="p">(</span><span class="nb">hash</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">update!</span><span class="p">(</span><span class="ss">week_stories_json</span><span class="p">:</span> <span class="nb">hash</span><span class="o">.</span><span class="n">to_json</span><span class="p">,</span> <span class="ss">expires_at</span><span class="p">:</span> <span class="no">EXPIRY_DURATION</span><span class="o">.</span><span class="n">from_now</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">resolve_story_reference</span><span class="p">(</span><span class="n">ref</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">ref</span><span class="o">.</span><span class="n">match?</span><span class="p">(</span><span class="sr">/\A@[a-f0-9]+\z/i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Hash reference (stable across re-listings)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">hash</span> <span class="o">=</span> <span class="n">ref</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="s2">&#34;@&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">downcase</span>
</span></span><span class="line"><span class="cl">      <span class="n">week_stories</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">find</span> <span class="p">{</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span> <span class="n">md5_prefix</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="o">==</span> <span class="nb">hash</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">elsif</span> <span class="n">ref</span><span class="o">.</span><span class="n">match?</span><span class="p">(</span><span class="sr">/\A#?\d+\z/</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">week_stories</span><span class="o">[</span><span class="n">ref</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="s2">&#34;#&#34;</span><span class="p">)</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada canal tem seu próprio contexto com expiração de 2 horas. O <code>/week</code> armazena o mapeamento número→arquivo. Depois, <code>/load #3</code> consulta esse contexto. Se expirou, o bot diz &ldquo;run <code>/week</code> first&rdquo;.</p>
<p>E o detalhe mais útil: referências por hash (<code>@a3f2b1</code>) ao invés de número. Quando você deleta a história #2, a #3 vira #2 — mas o hash <code>@a3f2b1</code> continua apontando pro mesmo arquivo. Isso evita o clássico &ldquo;deletei a errada porque a lista mudou&rdquo;.</p>
<h2>Padrão 5: Admin Check Simples<span class="hx:absolute hx:-mt-20" id="padrão-5-admin-check-simples"></span>
    <a href="#padr%c3%a3o-5-admin-check-simples" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nada de RBAC complexo. Uma lista de IDs:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">ADMIN_COMMANDS</span> <span class="o">=</span> <span class="o">%</span><span class="n">i</span><span class="o">[</span><span class="n">delete</span> <span class="n">update_score</span> <span class="n">approve</span> <span class="n">newsletter</span> <span class="n">publish_blog</span><span class="o">].</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">dispatch</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="ss">channel_id</span><span class="p">:,</span> <span class="ss">author_id</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="no">ADMIN_COMMANDS</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">command</span><span class="o">.</span><span class="n">type</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">admin?</span><span class="p">(</span><span class="n">author_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">send_error</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="s2">&#34;Permission denied. Admin privileges required.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">admin?</span><span class="p">(</span><span class="n">author_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">admin_ids</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;DISCORD_ADMIN_USER_IDS&#34;</span><span class="o">].</span><span class="n">to_s</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="n">admin_ids</span><span class="o">.</span><span class="n">empty?</span>  <span class="c1"># dev: sem IDs = tudo permitido</span>
</span></span><span class="line"><span class="cl">  <span class="n">admin_ids</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">author_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Em dev, sem <code>DISCORD_ADMIN_USER_IDS</code> configurado, tudo é permitido. Em produção, é uma lista de Discord user IDs. Sem banco, sem roles table, sem middleware. Dez linhas resolvem autorização.</p>
<h2>Padrão 6: Operações Assíncronas com Feedback<span class="hx:absolute hx:-mt-20" id="padrão-6-operações-assíncronas-com-feedback"></span>
    <a href="#padr%c3%a3o-6-opera%c3%a7%c3%b5es-ass%c3%adncronas-com-feedback" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Operações lentas (gerar imagem, processar artigo, chamar IA) vão pra background jobs. O feedback visual é em três tempos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">handle_ask</span><span class="p">(</span><span class="n">personality</span><span class="p">,</span> <span class="n">question</span><span class="p">,</span> <span class="n">attachments</span><span class="p">,</span> <span class="n">channel_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 1. Feedback imediato: &#34;estou pensando&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">thinking_id</span> <span class="o">=</span> <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;Thinking...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 2. Job faz o trabalho pesado em background</span>
</span></span><span class="line"><span class="cl">  <span class="no">AskJob</span><span class="o">.</span><span class="n">perform_later</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">question</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">channel_id</span><span class="p">:</span> <span class="n">channel_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">thinking_message_id</span><span class="p">:</span> <span class="n">thinking_id</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># No AskJob:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="ss">channel_id</span><span class="p">:,</span> <span class="ss">thinking_message_id</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">result</span> <span class="o">=</span> <span class="no">AiChat</span><span class="o">.</span><span class="n">ask_multimodal</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="ss">tools</span><span class="p">:</span> <span class="o">[</span><span class="no">GenerateImageTool</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. Remove &#34;Thinking...&#34; e mostra resultado</span>
</span></span><span class="line"><span class="cl">  <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">delete_message</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="n">thinking_message_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;**Marvin:** </span><span class="si">#{</span><span class="n">result</span><span class="o">[</span><span class="ss">:text</span><span class="o">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-47-26.jpg" alt="thinking"  loading="lazy" /></p>
<p>O &ldquo;Thinking&hellip;&rdquo; aparece instantaneamente, o job roda em background, e quando termina, a mensagem temporária some e o resultado real aparece. Se o job falhar, o &ldquo;Thinking&hellip;&rdquo; some e um embed de erro aparece.</p>
<p>Sem isso, o usuário manda uma pergunta e fica olhando pra tela sem saber se o bot ouviu. UX terrível.</p>
<h2>Padrão 7: RubyLLM + Tool Calling No Discord<span class="hx:absolute hx:-mt-20" id="padrão-7-rubyllm--tool-calling-no-discord"></span>
    <a href="#padr%c3%a3o-7-rubyllm--tool-calling-no-discord" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A parte mais poderosa: dar ferramentas pro LLM. O <code>/ask</code> tem três tools disponíveis: <code>GenerateImageTool</code> (gera imagens via Gemini), <code>WebSearchTool</code> (busca na web via Brave Search), e <code>WebFetchTool</code> (busca e resume páginas web). O LLM decide sozinho qual usar baseado na pergunta. Quando o usuário pede &ldquo;gera uma imagem de um gato programando&rdquo;, o LLM escolhe o tool certo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">GenerateImageTool</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Generate an image using Gemini. Use when the user asks to create or draw.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:prompt</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Image description&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:aspect_ratio</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;1:1 or 16:9&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">prompt</span><span class="p">:,</span> <span class="ss">aspect_ratio</span><span class="p">:</span> <span class="s2">&#34;1:1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">image_data</span> <span class="o">=</span> <span class="no">ImageGenerator</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="ss">prompt</span><span class="p">:</span> <span class="n">prompt</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Failed to generate image&#34;</span> <span class="k">unless</span> <span class="n">image_data</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="n">save_temp_file</span><span class="p">(</span><span class="n">image_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">Thread</span><span class="o">.</span><span class="n">current</span><span class="o">[</span><span class="ss">:generated_images</span><span class="o">]</span> <span class="o">&lt;&lt;</span> <span class="n">path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Image generated successfully. Tell the user it&#39;s ready.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>Thread.current[:generated_images]</code> é o truque pra passar dados de volta do tool pro job. O LLM chama o tool durante <code>chat.ask</code>, o tool salva o arquivo e registra o path. Depois que <code>chat.ask</code> retorna, o job pega os paths e manda pro Discord como file uploads.</p>
<p>Por que thread-local e não retorno direto? Porque o LLM pode chamar múltiplos tools numa única resposta, e o retorno do tool volta pro LLM (não pro seu código) — é ele que decide o que responder ao usuário. O thread-local é o side channel pra seu código saber o que aconteceu.</p>
<h2>Padrão 8: Notificações de Status Em Produção<span class="hx:absolute hx:-mt-20" id="padrão-8-notificações-de-status-em-produção"></span>
    <a href="#padr%c3%a3o-8-notifica%c3%a7%c3%b5es-de-status-em-produ%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo job de geração de conteúdo notifica um canal dedicado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DiscordStatus</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_start</span><span class="p">(</span><span class="n">label</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_to_status_channel</span><span class="p">(</span><span class="s2">&#34;Starting </span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2">...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_done</span><span class="p">(</span><span class="n">label</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_to_status_channel</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2"> complete.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_error</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_to_status_channel</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2"> FAILED: </span><span class="si">#{</span><span class="n">error</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O canal de status é separado do canal de operação. Quando os 8 jobs de geração de conteúdo rodam no domingo às 17h, o canal de status vira um log em tempo real:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Starting Anime Ranking...
Starting Hacker News...
Starting YouTube Digest...
Anime Ranking complete.
YouTube Digest complete.
Hacker News complete.
Starting Market Recap...  (wave 2, after world events)
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>De manhã, se algo deu errado, você abre o Discord no celular e sabe exatamente o quê — sem SSH no servidor, sem grep em logs.</p>
<p>E quando o bot reinicia após um deploy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">ready</span> <span class="k">do</span> <span class="o">|</span><span class="n">_event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">status_channel</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;DISCORD_STATUS_CHANNEL_ID&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">status_channel</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;Bot reconnected and ready.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-48-20.jpg" alt="log"  loading="lazy" /></p>
<p>Simples, mas você sabe que o deploy funcionou sem checar nada.</p>
<h2>Padrão 9: Cross-App Communication<span class="hx:absolute hx:-mt-20" id="padrão-9-cross-app-communication"></span>
    <a href="#padr%c3%a3o-9-cross-app-communication" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema tem dois apps Rails que precisam se falar. A newsletter precisa notificar o Discord quando termina de montar uma edição. Mas a newsletter não tem conexão Discord — quem tem é o marvin-bot.</p>
<p>Solução: HTTP relay.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Na newsletter — manda pro marvin-bot</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DiscordRelayClient</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">send_embed</span><span class="p">(</span><span class="ss">title</span><span class="p">:,</span> <span class="ss">description</span><span class="p">:,</span> <span class="ss">color</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span><span class="p">(</span><span class="s2">&#34;/api/discord_notify&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">type</span><span class="p">:</span> <span class="s2">&#34;embed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">payload</span><span class="p">:</span> <span class="p">{</span> <span class="ss">title</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span> <span class="ss">description</span><span class="p">:</span> <span class="n">description</span><span class="p">,</span> <span class="ss">color</span><span class="p">:</span> <span class="n">color</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># No marvin-bot — recebe e repassa pro Discord</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Api</span><span class="o">::</span><span class="no">DiscordNotifyController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">create</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">params</span><span class="o">[</span><span class="ss">:type</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s2">&#34;embed&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_embed</span><span class="p">(</span><span class="n">status_channel</span><span class="p">,</span> <span class="n">params</span><span class="o">[</span><span class="ss">:payload</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s2">&#34;message&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">status_channel</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:payload</span><span class="o">][</span><span class="ss">:content</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A newsletter fala HTTP. O marvin-bot traduz pra Discord. Um <code>ADMIN_TOKEN</code> compartilhado autentica. Todos os métodos resgatam erros e retornam nil — se o Discord estiver fora, a newsletter não trava. Fire and forget.</p>
<p>Mas tem um segundo fluxo cross-app mais interessante: o trigger de podcast. Quando a newsletter é aprovada, ela precisa avisar o marvin-bot pra gerar o podcast:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Na newsletter — dispara geração de podcast no marvin-bot</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MarvinBotClient</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">trigger_podcast</span><span class="p">(</span><span class="ss">date</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span><span class="p">(</span><span class="s2">&#34;/api/trigger/podcast&#34;</span><span class="p">,</span> <span class="ss">date</span><span class="p">:</span> <span class="n">date</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso inicia um pipeline inteiro no marvin-bot: script LLM de duas passadas → TTS por turno na GPU → assembly com loudnorm → upload S3. E o <code>PublishAndSendJob</code> da newsletter fica esperando (via <code>retry_on PodcastNotReady</code>) até o podcast ficar pronto. Dois apps, duas linguagens de backend (Ruby e Python), conectados por HTTP fire-and-forget com polling de resultado. Não é elegante — mas funciona toda semana sem intervenção.</p>
<h2>Padrão 10: Cost Tracking Integrado<span class="hx:absolute hx:-mt-20" id="padrão-10-cost-tracking-integrado"></span>
    <a href="#padr%c3%a3o-10-cost-tracking-integrado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cada chamada de IA é rastreada com tokens e custo estimado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">create_chat</span><span class="p">(</span><span class="nb">caller</span><span class="p">:,</span> <span class="ss">model</span><span class="p">:,</span> <span class="ss">provider</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span> <span class="ss">provider</span><span class="p">:</span> <span class="n">provider</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span><span class="o">.</span><span class="n">on_end_message</span> <span class="k">do</span> <span class="o">|</span><span class="n">message</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="no">ApiUsage</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="ss">caller_name</span><span class="p">:</span> <span class="nb">caller</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">input_tokens</span><span class="p">:</span> <span class="n">message</span><span class="o">.</span><span class="n">input_tokens</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">output_tokens</span><span class="p">:</span> <span class="n">message</span><span class="o">.</span><span class="n">output_tokens</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">cost_usd</span><span class="p">:</span> <span class="n">estimate_cost</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E no Discord: <code>/cost</code> mostra o gasto por mês, ano, e por provider. Sem dashboard separado, sem Grafana, sem nada. Um comando e a informação que importa.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-49-05.jpg" alt="cost"  loading="lazy" /></p>
<h2>Padrão 11: Nunca Falhe Silenciosamente<span class="hx:absolute hx:-mt-20" id="padrão-11-nunca-falhe-silenciosamente"></span>
    <a href="#padr%c3%a3o-11-nunca-falhe-silenciosamente" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse é o padrão que eu devia ter implementado desde o começo, mas só aprendi da pior maneira — em produção.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-50-46.jpg" alt="daily"  loading="lazy" /></p>
<p>O projeto tem ~10 daily jobs que buscam dados de fontes externas (Hacker News, GitHub Trending, Morningstar, etc.) e postam embeds no Discord. Cada client segue um padrão &ldquo;defensivo&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># O padrão que PARECE correto:</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DailyDevClient</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch_popular</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="no">HTTParty</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="no">API_URL</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&#34;Authorization&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;Bearer </span><span class="si">#{</span><span class="n">token</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">[]</span> <span class="k">unless</span> <span class="n">response</span><span class="o">.</span><span class="n">success?</span>
</span></span><span class="line"><span class="cl">    <span class="n">parse</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;DailyDevClient failed: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">[]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># E no job:</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DailyDailyDevJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">    <span class="n">posts</span> <span class="o">=</span> <span class="no">DailyDevClient</span><span class="o">.</span><span class="n">fetch_popular</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">if</span> <span class="n">posts</span><span class="o">.</span><span class="n">empty?</span>  <span class="c1"># ← O PROBLEMA</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... posta embed no Discord</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse código nunca crasheia. Nunca levanta exceção. Nunca enche o log de erros. É o código mais &ldquo;robusto&rdquo; possível.</p>
<p>É também completamente <strong>inútil pra diagnóstico</strong>. Quando deployei e rodei os daily jobs manualmente, só um dos dez funcionou. Mas não havia <strong>nenhum</strong> erro nos logs. Nenhum. O <code>return [] unless response.success?</code> engolia o HTTP 401 sem logar. O <code>return if posts.empty?</code> descartava o resultado sem avisar ninguém. Tudo verde, tudo silencioso, zero dados.</p>
<p>A correção tem duas partes:</p>
<p><strong>Parte 1: Clients logam o código HTTP:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">unless</span> <span class="n">response</span><span class="o">.</span><span class="n">success?</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;DailyDevClient: API returned HTTP </span><span class="si">#{</span><span class="n">response</span><span class="o">.</span><span class="n">code</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Parece óbvio, mas é fácil esquecer quando você escreve <code>return []</code> como reflexo defensivo. O <code>response.code</code> é a informação que diferencia &ldquo;token expirado&rdquo; (401) de &ldquo;rate limited&rdquo; (429) de &ldquo;API fora do ar&rdquo; (503). Sem ele, tudo parece igual: array vazio.</p>
<p><strong>Parte 2: Jobs avisam no Discord quando não têm dados:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DailyDiscordEmbed</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">report_empty</span><span class="p">(</span><span class="n">title</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">title</span><span class="si">}</span><span class="s2"> returned no results&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">channel_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">discord</span><span class="o">.</span><span class="n">channel_ids</span><span class="o">.</span><span class="n">first</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">unless</span> <span class="n">channel_id</span><span class="o">.</span><span class="n">present?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">embed</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">title</span><span class="p">:</span> <span class="s2">&#34;⚠️ </span><span class="si">#{</span><span class="n">title</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">description</span><span class="p">:</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2"> returned no results. Check logs.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">color</span><span class="p">:</span> <span class="mh">0xFFA500</span><span class="p">,</span>  <span class="c1"># laranja = warning, não vermelho = error</span>
</span></span><span class="line"><span class="cl">      <span class="ss">timestamp</span><span class="p">:</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="o">.</span><span class="n">iso8601</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_embed</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="n">embed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2"> empty report failed: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora quando um daily job não traz dados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">  <span class="n">posts</span> <span class="o">=</span> <span class="no">DailyDevClient</span><span class="o">.</span><span class="n">fetch_popular</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">posts</span><span class="o">.</span><span class="n">empty?</span>
</span></span><span class="line"><span class="cl">    <span class="n">report_empty</span><span class="p">(</span><span class="s2">&#34;daily.dev Popular&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-50-02.jpg" alt="no results"  loading="lazy" /></p>
<p>Um embed laranja aparece no Discord. De manhã, abro o canal e vejo: &ldquo;⚠️ daily.dev Popular returned no results&rdquo;. Vou nos logs e descubro: &ldquo;DailyDevClient: API returned HTTP 401&rdquo;. Token expirado. Corrigido em 2 minutos.</p>
<p>Sem esse padrão, eu teria descoberto semanas depois — quando alguém perguntasse &ldquo;por que nunca tem posts do daily.dev?&rdquo;.</p>
<p>A lição: <strong><code>rescue =&gt; []</code> não é error handling. É error hiding.</strong> Cada ponto de falha precisa de dois sinais: um log com o detalhe técnico (HTTP code, mensagem de exceção) e uma notificação visual no canal onde você já está olhando (Discord). Se o bot É sua interface operacional, falhas do bot precisam aparecer NA interface.</p>
<h2>Padrão 12: Conversas Com Memória (<code>/ask</code> Multi-Turn)<span class="hx:absolute hx:-mt-20" id="padrão-12-conversas-com-memória-ask-multi-turn"></span>
    <a href="#padr%c3%a3o-12-conversas-com-mem%c3%b3ria-ask-multi-turn" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A primeira versão do <code>/ask</code> criava um <code>RubyLLM::Chat</code> novo a cada invocação. Funcionava, mas era como conversar com alguém que tem amnésia:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/ask o que é Ruby?
→ Ruby é uma linguagem de programação criada por Matz em 1995...

/ask compare com Python
→ Comparar o quê com Python? Poderia ser mais específico?</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O ruby_llm já mantém histórico internamente — cada <code>chat.ask()</code> appende à lista de mensagens e manda o contexto completo pro modelo. O que faltava era persistir o objeto <code>Chat</code> entre invocações.</p>
<p>A solução é um store in-memory com três safeguards:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">AskSessionStore</span>
</span></span><span class="line"><span class="cl">  <span class="no">MAX_SESSIONS</span> <span class="o">=</span> <span class="mi">100</span>       <span class="c1"># total across all users</span>
</span></span><span class="line"><span class="cl">  <span class="no">SESSION_TTL</span> <span class="o">=</span> <span class="mi">2</span><span class="o">.</span><span class="n">hours</span>    <span class="c1"># inactivity timeout</span>
</span></span><span class="line"><span class="cl">  <span class="no">MAX_MESSAGES</span> <span class="o">=</span> <span class="mi">50</span>        <span class="c1"># ~25 exchanges, then auto-reset</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="no">Session</span> <span class="o">=</span> <span class="no">Struct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">:chat</span><span class="p">,</span> <span class="ss">:last_used</span><span class="p">,</span> <span class="ss">keyword_init</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">get_or_create</span><span class="p">(</span><span class="n">author_id</span><span class="p">,</span> <span class="n">personality</span><span class="p">,</span> <span class="ss">system_prompt</span><span class="p">:,</span> <span class="ss">tools</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">synchronize</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">cleanup_expired</span>
</span></span><span class="line"><span class="cl">      <span class="n">key</span> <span class="o">=</span> <span class="o">[</span><span class="n">author_id</span><span class="p">,</span> <span class="n">personality</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="n">session</span> <span class="o">=</span> <span class="vi">@sessions</span><span class="o">[</span><span class="n">key</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">session</span><span class="o">.</span><span class="n">last_used</span> <span class="o">=</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">session</span><span class="o">.</span><span class="n">chat</span><span class="o">.</span><span class="n">messages</span><span class="o">.</span><span class="n">length</span> <span class="o">&gt;</span> <span class="no">MAX_MESSAGES</span>
</span></span><span class="line"><span class="cl">          <span class="vi">@sessions</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="k">return</span> <span class="n">create_session</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">system_prompt</span><span class="p">:</span> <span class="n">system_prompt</span><span class="p">,</span> <span class="ss">tools</span><span class="p">:</span> <span class="n">tools</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">        <span class="n">session</span><span class="o">.</span><span class="n">chat</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="n">evict_oldest</span> <span class="k">if</span> <span class="vi">@sessions</span><span class="o">.</span><span class="n">size</span> <span class="o">&gt;=</span> <span class="no">MAX_SESSIONS</span>
</span></span><span class="line"><span class="cl">        <span class="n">create_session</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">system_prompt</span><span class="p">:</span> <span class="n">system_prompt</span><span class="p">,</span> <span class="ss">tools</span><span class="p">:</span> <span class="n">tools</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-54-32.jpg" alt="ask memory"  loading="lazy" /></p>
<p>A chave vai ser <code>(author_id, personality)</code> — cada usuário tem sessões separadas pro Marvin e pro Akita. Volátil de propósito: reiniciou o servidor, perdeu o histórico. Sem banco, sem serialização, sem complexidade. É memória de trabalho, não persistência.</p>
<p>O <code>/ask clear</code> reseta a sessão. Sem isso, não tem como &ldquo;começar de novo&rdquo; quando a conversa fica confusa.</p>
<p>Agora:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/ask o que é Ruby?
→ Ruby é uma linguagem de programação criada por Matz em 1995...

/ask compare com Python
→ Ruby e Python têm filosofias parecidas — ambas priorizam felicidade do
  programador — mas Ruby abraça blocos e metaprogramação enquanto Python
  prefere explicitness...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O LLM sabe que &ldquo;compare&rdquo; se refere a Ruby porque o histórico da sessão inclui a pergunta anterior. É a diferença entre um chatbot e um assistente.</p>
<h2>Padrão 13: Registrando Application Commands (Autocomplete Nativo)<span class="hx:absolute hx:-mt-20" id="padrão-13-registrando-application-commands-autocomplete-nativo"></span>
    <a href="#padr%c3%a3o-13-registrando-application-commands-autocomplete-nativo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A seção sobre Application Commands acima mostrou o conceito simplificado. Na prática, registrar 18 comandos com subcommands, parâmetros tipados e choices exige uma estrutura dedicada. Veja como o projeto resolve isso.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-43-18.jpg" alt="auto complete"  loading="lazy" /></p>
<h3>O Problema Com Texto Puro<span class="hx:absolute hx:-mt-20" id="o-problema-com-texto-puro"></span>
    <a href="#o-problema-com-texto-puro" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando o bot só parseia texto, o usuário precisa decorar a sintaxe:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/update score #1 #2 #3 high       ← funciona
/update score #1 #2 #3 alta       ← falha silenciosamente
/update scoree #1 high             ← ignorado</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sem autocomplete, sem validação de parâmetros, sem documentação inline. O usuário digita <code>/</code> e não sabe o que está disponível.</p>
<h3>O Módulo <code>DiscordCommands</code><span class="hx:absolute hx:-mt-20" id="o-módulo-discordcommands"></span>
    <a href="#o-m%c3%b3dulo-discordcommands" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Toda a definição de comandos vive num módulo separado com dois métodos públicos: <code>register!</code> (cadastra os comandos na API do Discord) e <code>setup_handlers!</code> (conecta os handlers ao bot):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DiscordCommands</span>
</span></span><span class="line"><span class="cl">  <span class="no">SCORE_CHOICES</span>  <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;High&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;high&#34;</span><span class="p">,</span> <span class="s2">&#34;Medium&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;medium&#34;</span><span class="p">,</span> <span class="s2">&#34;Low&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;low&#34;</span> <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">  <span class="no">SECTION_CHOICES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Tech&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;tech&#34;</span><span class="p">,</span> <span class="s2">&#34;Global&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;global&#34;</span><span class="p">,</span> <span class="s2">&#34;Financial&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;financial&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Q&amp;A&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;qa&#34;</span><span class="p">,</span> <span class="s2">&#34;Entertainment&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;entertainment&#34;</span><span class="p">,</span> <span class="s2">&#34;Open Source&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;opensource&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Product&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;product&#34;</span><span class="p">,</span> <span class="s2">&#34;Book&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;book&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">  <span class="no">PERSONALITY_CHOICES</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;Marvin (default)&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;marvin&#34;</span><span class="p">,</span> <span class="s2">&#34;Akita&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;akita&#34;</span> <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">server_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">discord</span><span class="o">.</span><span class="n">server_id</span>
</span></span><span class="line"><span class="cl">    <span class="n">sid</span> <span class="o">=</span> <span class="n">server_id</span><span class="o">.</span><span class="n">present?</span> <span class="p">?</span> <span class="n">server_id</span><span class="o">.</span><span class="n">to_i</span> <span class="p">:</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_week</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">register_add</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">register_update</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">register_ask</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... mais 14 comandos</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">setup_handlers!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_week_handlers</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_add_handler</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_update_handlers</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_ask_handlers</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... mais 14 handlers</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Guild vs Global<span class="hx:absolute hx:-mt-20" id="guild-vs-global"></span>
    <a href="#guild-vs-global" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>server_id</code> (ou <code>sid</code>) controla o escopo do registro. Com <code>server_id</code>, o comando aparece instantaneamente no servidor especificado. Sem, o comando é global — disponível em todos os servidores, mas pode levar até uma hora pra propagar.</p>
<p>Para um bot privado de operações internas, sempre use guild-scoped. O env var <code>DISCORD_SERVER_ID</code> é configurado no deploy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">sid</span> <span class="o">=</span> <span class="n">server_id</span><span class="o">.</span><span class="n">present?</span> <span class="p">?</span> <span class="n">server_id</span><span class="o">.</span><span class="n">to_i</span> <span class="p">:</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:week</span><span class="p">,</span> <span class="s2">&#34;Story list and weekly report&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se <code>sid</code> for nil, o discordrb registra globalmente. Em dev, é conveniente; em produção, guild-scoped é mais rápido e permite comandos diferentes por servidor.</p>
<h3>Anatomia De Um Comando<span class="hx:absolute hx:-mt-20" id="anatomia-de-um-comando"></span>
    <a href="#anatomia-de-um-comando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O método mais simples — sem parâmetros:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register_count</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:count</span><span class="p">,</span> <span class="s2">&#34;Show story count for this week&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com parâmetros tipados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register_add</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:add</span><span class="p">,</span> <span class="s2">&#34;Submit a URL for the newsletter&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:url</span><span class="p">,</span> <span class="s2">&#34;The URL to submit&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:tags</span><span class="p">,</span> <span class="s2">&#34;Tags (e.g. #tech #high)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:comment</span><span class="p">,</span> <span class="s2">&#34;Optional comment about this link&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O Discord mostra isso como um formulário inline: campo <code>url</code> obrigatório, <code>tags</code> e <code>comment</code> opcionais. O usuário vê a descrição de cada parâmetro enquanto preenche.</p>
<h3>Subcommands: Agrupando Ações<span class="hx:absolute hx:-mt-20" id="subcommands-agrupando-ações"></span>
    <a href="#subcommands-agrupando-a%c3%a7%c3%b5es" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Comandos complexos usam subcommands. <code>/update</code> sozinho não faz nada — o usuário precisa escolher o que atualizar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register_update</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:update</span><span class="p">,</span> <span class="s2">&#34;Update story properties&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:score</span><span class="p">,</span> <span class="s2">&#34;Update story priority score&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:level</span><span class="p">,</span> <span class="s2">&#34;Priority level&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="no">SCORE_CHOICES</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:tag</span><span class="p">,</span> <span class="s2">&#34;Update story section tag&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:section</span><span class="p">,</span> <span class="s2">&#34;Section tag&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="no">SECTION_CHOICES</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:image</span><span class="p">,</span> <span class="s2">&#34;Update story image&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">attachment</span><span class="p">(</span><span class="ss">:file</span><span class="p">,</span> <span class="s2">&#34;Image file (omit for AI generation)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... mais subcommands: title, description, comment, source</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando o usuário digita <code>/update</code>, o Discord mostra uma lista de subcommands: score, tag, title, description, comment, image, source. Ao escolher <code>score</code>, aparece o campo <code>stories</code> (texto livre) e <code>level</code> (dropdown com High/Medium/Low). Impossível errar a sintaxe.</p>
<h3>Choices: Dropdowns Nativos<span class="hx:absolute hx:-mt-20" id="choices-dropdowns-nativos"></span>
    <a href="#choices-dropdowns-nativos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O hash <code>SCORE_CHOICES</code> vira um dropdown no Discord. A chave é o label visível (&ldquo;High&rdquo;), o valor é o que chega no handler (&ldquo;high&rdquo;):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">SCORE_CHOICES</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;High&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;high&#34;</span><span class="p">,</span> <span class="s2">&#34;Medium&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;medium&#34;</span><span class="p">,</span> <span class="s2">&#34;Low&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;low&#34;</span> <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># No registro:</span>
</span></span><span class="line"><span class="cl"><span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:level</span><span class="p">,</span> <span class="s2">&#34;Priority level&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="no">SCORE_CHOICES</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O usuário não precisa lembrar se é &ldquo;high&rdquo;, &ldquo;HIGH&rdquo;, &ldquo;alta&rdquo;, ou &ldquo;3&rdquo;. Seleciona no dropdown e pronto. Isso elimina uma categoria inteira de erros de input.</p>
<h3>Attachments: Upload de Arquivos<span class="hx:absolute hx:-mt-20" id="attachments-upload-de-arquivos"></span>
    <a href="#attachments-upload-de-arquivos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Discord suporta parâmetros do tipo attachment — o usuário pode arrastar um arquivo direto no comando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:image</span><span class="p">,</span> <span class="s2">&#34;Update story image&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nb">sub</span><span class="o">.</span><span class="n">attachment</span><span class="p">(</span><span class="ss">:file</span><span class="p">,</span> <span class="s2">&#34;Image file (omit for AI generation)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No handler, os attachments resolvidos vêm como URLs do CDN do Discord:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">resolve_attachments</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">[]</span> <span class="k">unless</span> <span class="n">event</span><span class="o">.</span><span class="n">resolved</span><span class="o">&amp;.</span><span class="n">attachments</span><span class="o">&amp;.</span><span class="n">any?</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">resolved</span><span class="o">.</span><span class="n">attachments</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Lifecycle: Onde Registrar, Onde Conectar Handlers<span class="hx:absolute hx:-mt-20" id="lifecycle-onde-registrar-onde-conectar-handlers"></span>
    <a href="#lifecycle-onde-registrar-onde-conectar-handlers" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse é o ponto que mais confunde. Os <strong>handlers</strong> precisam ser registrados <strong>antes</strong> do <code>bot.run</code>. O <strong>registro na API do Discord</strong> pode acontecer no <code>bot.ready</code> (que dispara após a conexão WebSocket):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MarvinBotRunner</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">run!</span>
</span></span><span class="line"><span class="cl">    <span class="n">bot</span> <span class="o">=</span> <span class="no">Discordrb</span><span class="o">::</span><span class="no">Bot</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">token</span><span class="p">:</span> <span class="n">token</span><span class="p">,</span> <span class="ss">intents</span><span class="p">:</span> <span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 1. Handlers ANTES do bot.run (senão o bot não sabe como responder)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordCommands</span><span class="o">.</span><span class="n">setup_handlers!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">bot</span><span class="o">.</span><span class="n">ready</span> <span class="k">do</span> <span class="o">|</span><span class="n">_event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># 2. Registro na API DEPOIS da conexão (idempotente, safe on reconnect)</span>
</span></span><span class="line"><span class="cl">      <span class="no">DiscordCommands</span><span class="o">.</span><span class="n">register!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">bot</span><span class="o">.</span><span class="n">run</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por que no <code>bot.ready</code>? Porque o bot pode reconectar (Discord fecha WebSockets periodicamente). O <code>register!</code> é idempotente — registrar o mesmo comando duas vezes não duplica, só atualiza. Colocar no <code>ready</code> garante que os comandos existem mesmo após um reconnect.</p>
<h3>Bridge: Application Command → CommandDispatcher<span class="hx:absolute hx:-mt-20" id="bridge-application-command--commanddispatcher"></span>
    <a href="#bridge-application-command--commanddispatcher" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O ponto mais elegante da arquitetura: o handler de Application Command não tem lógica de negócio. Ele traduz os parâmetros do Discord pro formato que o <code>CommandDispatcher</code> já espera:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">dispatch_interaction</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">type</span><span class="p">,</span> <span class="ss">match</span><span class="p">:</span> <span class="o">[</span><span class="kp">nil</span><span class="o">]</span><span class="p">,</span> <span class="ss">attachments</span><span class="p">:</span> <span class="o">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">respond</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;\u{2705}&#34;</span><span class="p">,</span> <span class="ss">ephemeral</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>  <span class="c1"># checkmark só visível pro autor</span>
</span></span><span class="line"><span class="cl">  <span class="n">command</span> <span class="o">=</span> <span class="no">SlashCommandParser</span><span class="o">::</span><span class="no">Command</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="ss">type</span><span class="p">:</span> <span class="n">type</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">args</span><span class="p">:</span> <span class="p">{</span> <span class="ss">match</span><span class="p">:</span> <span class="n">match</span><span class="p">,</span> <span class="ss">attachments</span><span class="p">:</span> <span class="n">attachments</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">CommandDispatcher</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">command</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">channel_id</span><span class="p">:</span> <span class="n">event</span><span class="o">.</span><span class="n">channel</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">message_id</span><span class="p">:</span> <span class="kp">nil</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">author_id</span><span class="p">:</span> <span class="n">event</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&#34;Discord interaction error (</span><span class="si">#{</span><span class="n">type</span><span class="si">}</span><span class="s2">): </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada handler de subcommand é um one-liner que monta os argumentos e chama <code>dispatch_interaction</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">application_command</span><span class="p">(</span><span class="ss">:update</span><span class="p">)</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:score</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">stories</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">options</span><span class="o">[</span><span class="s2">&#34;stories&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">level</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">options</span><span class="o">[</span><span class="s2">&#34;level&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">dispatch_interaction</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="ss">:update_score</span><span class="p">,</span> <span class="ss">match</span><span class="p">:</span> <span class="o">[</span><span class="kp">nil</span><span class="p">,</span> <span class="n">stories</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="n">level</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>CommandDispatcher</code> não sabe se o comando veio de texto puro ou de Application Command. A mesma lógica, a mesma autorização, os mesmos jobs. Nenhum código duplicado.</p>
<h3>O Resultado<span class="hx:absolute hx:-mt-20" id="o-resultado"></span>
    <a href="#o-resultado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O projeto registra 18 Application Commands com combinações de subcommands, parâmetros tipados, dropdowns e upload de arquivos. O usuário digita <code>/</code> e vê tudo disponível com descrição. Escolhe um comando e o Discord guia o preenchimento campo a campo. Erros de sintaxe são impossíveis — o Discord valida antes de enviar.</p>
<p>E os dois sistemas coexistem: quem prefere autocomplete usa Application Commands, quem prefere digitar rápido usa texto puro. O <code>CommandDispatcher</code> recebe os dois da mesma forma. Adicionar um novo comando é adicionar um <code>register_*</code>, um <code>setup_*_handler</code>, e um método no dispatcher — sem mexer no runner.</p>
<h2>As Armadilhas do discordrb<span class="hx:absolute hx:-mt-20" id="as-armadilhas-do-discordrb"></span>
    <a href="#as-armadilhas-do-discordrb" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nem tudo são flores. Algumas dores reais:</p>
<p><strong>Gateway disconnects</strong>: O WebSocket com o Discord cai de vez em quando. O discordrb reconecta automaticamente, mas seus handlers <code>bot.ready</code> e <code>bot.message</code> precisam ser resilientes a reconexões.</p>
<p><strong>Message Content Intent</strong>: Desde 2022, bots em mais de 75 servidores precisam de aprovação pra ler conteúdo de mensagens. Pra bots privados (que é o caso de operação interna), isso não é problema. Mas se você planeja distribuir o bot, prepare o paperwork.</p>
<p><strong>API Rate Limits</strong>: O Discord rate-limita por rota. Mandar 10 embeds de uma vez pro mesmo canal? Vai bater no limite. O discordrb faz rate limit handling internamente, mas chamadas rápidas demais causam delays visíveis.</p>
<p><strong>Embed Limits</strong>: Título máximo 256 chars. Description 4096. Field value 1024. Excedeu? Erro silencioso. Sempre trunca antes de enviar.</p>
<p><strong>File Uploads</strong>: Limit de 25MB no plano gratuito, 50MB com Nitro. Pra imagens de IA geradas isso nunca é problema, mas se for mandar PDFs ou áudio, cuidado.</p>
<h2>Quando NÃO Usar Discord Como Admin<span class="hx:absolute hx:-mt-20" id="quando-não-usar-discord-como-admin"></span>
    <a href="#quando-n%c3%a3o-usar-discord-como-admin" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><strong>Formulários complexos</strong>: editar 15 campos de um registro? Tela web</li>
<li><strong>Bulk operations</strong>: importar CSV com 1000 registros? Script</li>
<li><strong>Visualização de dados</strong>: gráficos, dashboards analíticos? Grafana/Metabase</li>
<li><strong>Aprovação multi-step</strong>: workflows com 5 etapas de aprovação? Precisa de estado persistido</li>
</ul>
<p>Mas pra o dia a dia operacional — curar conteúdo, aprovar newsletters, monitorar jobs, checar custos, conversar com IA — Discord é a interface mais produtiva que já usei. E o melhor: os assinantes nunca vão saber que a newsletter semanal deles foi curada por um cara digitando comandos no Discord do celular enquanto toma café.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-57-11.jpg" alt="hello"  loading="lazy" /></p>
<p>Usar Discord como admin panel não é preguiça — é pragmatismo. A melhor interface é a que você já usa o dia inteiro. Auth, push notifications, mobile, rich formatting, file sharing — tudo de graça. Você só constrói a lógica de negócio.</p>
<p>A gem <a href="https://rubyllm.com/"target="_blank" rel="noopener"><code>ruby_llm</code></a> com tool calling leva o bot de &ldquo;executor de comandos&rdquo; pra &ldquo;assistente que pensa&rdquo;. Com memória de sessão, pensa <strong>e lembra</strong>. Quando o LLM pode gerar imagens, buscar na web, manter contexto entre perguntas, e responder sobre a arquitetura do próprio sistema — aí deixou de ser CRUD e virou co-piloto.</p>
<p>E o padrão Parser → Dispatcher → Job mantém tudo testável. O parser não sabe nada de Discord. O dispatcher não sabe nada de IA. Os jobs não sabem nada de interface. Troca qualquer uma dessas camadas e as outras nem percebem.</p>
<p>No fim, a melhor admin UI é a que não precisa de deploy separado. E o pior bug é o que falha em silêncio — se o bot é sua interface operacional, as falhas do bot precisam aparecer <strong>na</strong> interface, não escondidas num log que ninguém lê.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>automacao</category><category>backend</category></item><item><title>Web Scrapping em 2026 | Bastidores do The M.Akita Chronicles</title><link>https://akitaonrails.github.io/2026/02/18/web-scrapping-em-2026-bastidores-do-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/18/web-scrapping-em-2026-bastidores-do-the-m-akita-chronicles/</guid><pubDate>Wed, 18 Feb 2026 14:57:22 +0000</pubDate><description>&lt;p&gt;Este post vai fazer parte de uma série; acompanhe pela tag &lt;a href="https://akitaonrails.github.io/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. Esta é a parte 2.&lt;/p&gt;
&lt;p&gt;E não deixe de assinar minha nova newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Dez anos atrás, buscar dados em um site era trivial. Quatro linhas de Ruby: &lt;code&gt;Net::HTTP.get&lt;/code&gt;, Nokogiri parse, CSS selector, pronto. O mundo inteiro estava aberto pra qualquer script com um User-Agent decente.&lt;/p&gt;
&lt;p&gt;Hoje? Metade dos sites que preciso ler pra montar minha newsletter retorna &amp;ldquo;Are you a robot?&amp;rdquo; pra qualquer requisição HTTP convencional. A outra metade renderiza o conteúdo inteiramente em JavaScript — não há HTML nenhum na resposta do servidor. E os mais paranoicos combinam as duas coisas.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post vai fazer parte de uma série; acompanhe pela tag <a href="/tags/the-m-akita-chronicles/">/themakitachronicles</a>. Esta é a parte 2.</p>
<p>E não deixe de assinar minha nova newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Dez anos atrás, buscar dados em um site era trivial. Quatro linhas de Ruby: <code>Net::HTTP.get</code>, Nokogiri parse, CSS selector, pronto. O mundo inteiro estava aberto pra qualquer script com um User-Agent decente.</p>
<p>Hoje? Metade dos sites que preciso ler pra montar minha newsletter retorna &ldquo;Are you a robot?&rdquo; pra qualquer requisição HTTP convencional. A outra metade renderiza o conteúdo inteiramente em JavaScript — não há HTML nenhum na resposta do servidor. E os mais paranoicos combinam as duas coisas.</p>
<p>Meu objetivo era criar uma rotina diária pro M.arvin me devolver as principais notícias pela manhã no meu Discord. Assim eu não preciso sair caçando as notícias do dia. Parecia simples… no começo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-18_11-59-42.jpg" alt="daily routine"  loading="lazy" /></p>
<p>Este post é sobre como passei de &ldquo;uma chamada HTTP&rdquo; pra &ldquo;empacotar um Chromium inteiro no Docker&rdquo; — e por que isso é a realidade de qualquer projeto que precisa ler a web programaticamente em 2026.</p>
<h2>A Idade de Ouro: HTTP Simples<span class="hx:absolute hx:-mt-20" id="a-idade-de-ouro-http-simples"></span>
    <a href="#a-idade-de-ouro-http-simples" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos começar pelo que ainda funciona. Alguns serviços oferecem dados estruturados sem nenhuma resistência:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># RSS feeds: o formato mais resiliente da web</span>
</span></span><span class="line"><span class="cl"><span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="s2">&#34;https://news.google.com/rss/search?q=when:24h+allinurl:reuters.com/world&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">xml</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="no">REXML</span><span class="o">::</span><span class="no">Document</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">xml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="o">.</span><span class="n">get_elements</span><span class="p">(</span><span class="s2">&#34;//item&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span>   <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;link&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># APIs públicas: JSON limpo, documentado, estável</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="no">HTTParty</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;https://api.jikan.moe/v4/top/anime?filter=airing&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">animes</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">parsed_response</span><span class="o">[</span><span class="s2">&#34;data&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># GitHub trending: HTML server-rendered sem proteção</span>
</span></span><span class="line"><span class="cl"><span class="n">html</span> <span class="o">=</span> <span class="no">HTTParty</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;https://github.com/trending&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">body</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">repos</span> <span class="o">=</span> <span class="n">doc</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="s2">&#34;article.Box-row&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Hacker News, GitHub Trending, APIs de anime, feeds RSS, Project Gutenberg — todos funcionam com um <code>GET</code> e um parser. Sem JavaScript, sem autenticação implícita, sem fingerprinting. Assim como a web deveria ser.</p>
<p>O problema é que esses são a exceção. Os sites que têm dados financeiros, notícias exclusivas ou conteúdo premium — exatamente os dados que mais importam — estão cada vez mais blindados.</p>
<h2>O Primeiro Obstáculo: SPAs e JavaScript Rendering<span class="hx:absolute hx:-mt-20" id="o-primeiro-obstáculo-spas-e-javascript-rendering"></span>
    <a href="#o-primeiro-obst%c3%a1culo-spas-e-javascript-rendering" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando o servidor retorna algo tipo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;root&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;/app-3fa8c2.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Seu <code>Net::HTTP.get</code> retorna exatamente isso: uma div vazia e um link pra um bundle de 2 MB. O conteúdo real — artigos, preços, listas — é montado no browser por React, Vue, Angular ou o framework da semana.</p>
<p>Pra um script, essa página é inútil. Pra extrair o conteúdo, você precisa executar o JavaScript. Isso significa rodar um browser real.</p>
<p>No mundo Ruby, a resposta é a gem <code>ferrum</code> — um cliente CDP (Chrome DevTools Protocol) que controla o Chromium programaticamente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">browser</span> <span class="o">=</span> <span class="no">Ferrum</span><span class="o">::</span><span class="no">Browser</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">headless</span><span class="p">:</span> <span class="s2">&#34;new&#34;</span><span class="p">,</span> <span class="ss">timeout</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">browser</span><span class="o">.</span><span class="n">go_to</span><span class="p">(</span><span class="s2">&#34;https://www.morningstar.com/markets&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">browser</span><span class="o">.</span><span class="n">network</span><span class="o">.</span><span class="n">wait_for_idle</span>  <span class="c1"># espera o JS terminar de carregar</span>
</span></span><span class="line"><span class="cl"><span class="n">html</span> <span class="o">=</span> <span class="n">browser</span><span class="o">.</span><span class="n">body</span>  <span class="c1"># agora sim o DOM está populado</span>
</span></span><span class="line"><span class="cl"><span class="n">browser</span><span class="o">.</span><span class="n">quit</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse <code>browser.body</code> retorna o HTML <strong>depois</strong> que o JavaScript executou. Mesmos dados que você vê no browser, agora acessíveis pro seu parser.</p>
<p>Mas aqui começa a dor de cabeça: você precisa de um binário do Chromium disponível no sistema. No Docker, isso significa adicionar ~300MB à imagem. No servidor, significa mais uma dependência pra gerenciar. E a latência de abrir um browser, navegar, esperar JS renderizar, e extrair o HTML — estamos falando de 3 a 10 segundos por página, ao invés dos 200ms de um HTTP GET.</p>
<p>O projeto usa isso como estratégia principal no <code>ArticleFetcher</code>: primeiro tenta com browser (pra pegar SPAs), depois faz um fallback HTTP simples (pra pegar metadata OpenGraph quando o browser falha). A maioria dos links que os leitores mandam no Discord são de sites que renderizam com JavaScript, então o browser-first compensa.</p>
<h2>O Segundo Obstáculo: Bot Detection<span class="hx:absolute hx:-mt-20" id="o-segundo-obstáculo-bot-detection"></span>
    <a href="#o-segundo-obst%c3%a1culo-bot-detection" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Abrir uma página com Chromium headless resolve o problema de JavaScript. Mas resolve 50% dos sites — não 100%. Porque existe uma indústria inteira dedicada a distinguir browsers reais de browsers automatizados.</p>
<h3>O Que o Headless Chrome Revela<span class="hx:absolute hx:-mt-20" id="o-que-o-headless-chrome-revela"></span>
    <a href="#o-que-o-headless-chrome-revela" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Chromium em modo headless é quase idêntico ao Chrome normal. Quase. As diferenças são sutis, mas suficientes pra detecção:</p>
<p><strong>1. <code>navigator.webdriver</code></strong> — Em todo Chrome controlado por automação, essa propriedade retorna <code>true</code>. Em browsers normais, <code>undefined</code>. É o flag mais óbvio.</p>
<p><strong>2. <code>navigator.plugins</code></strong> — Chrome normal tem 3-5 plugins (PDF viewer, etc.). Headless retorna array vazio. Bot detectado.</p>
<p><strong>3. <code>navigator.userAgentData</code></strong> — A Client Hints API. Chrome real retorna brands, platform, mobile flag. Headless pode não ter isso.</p>
<p><strong>4. WebGL fingerprint</strong> — O renderer WebGL num headless Chrome retorna &ldquo;SwiftShader&rdquo; (renderizador por software). Chrome real retorna &ldquo;NVIDIA GeForce&rdquo; ou &ldquo;Intel Iris&rdquo; ou o que for. Bot detectado.</p>
<p><strong>5. Timing e behavior</strong> — Browsers reais demoram pra carregar. Fazem scroll. Movem o mouse. Têm focus events. Um script que faz <code>go_to</code> → <code>body</code> em 2 segundos não se parece com um humano.</p>
<p>A solução é o que chamo de StealthBrowser — um wrapper que injeta patches JavaScript antes de qualquer script da página rodar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">StealthBrowser</span>
</span></span><span class="line"><span class="cl">  <span class="no">STEALTH_JS</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">JS</span>
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Remove</span> <span class="n">o</span> <span class="n">flag</span> <span class="n">de</span> <span class="n">automação</span>
</span></span><span class="line"><span class="cl">    <span class="no">Object</span><span class="o">.</span><span class="n">defineProperty</span><span class="p">(</span><span class="n">navigator</span><span class="p">,</span> <span class="s1">&#39;webdriver&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="ss">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kp">false</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Simula</span> <span class="n">plugins</span> <span class="n">reais</span>
</span></span><span class="line"><span class="cl">    <span class="no">Object</span><span class="o">.</span><span class="n">defineProperty</span><span class="p">(</span><span class="n">navigator</span><span class="p">,</span> <span class="s1">&#39;plugins&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s1">&#39;Chrome PDF Plugin&#39;</span><span class="p">,</span> <span class="ss">filename</span><span class="p">:</span> <span class="s1">&#39;internal-pdf-viewer&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s1">&#39;Chrome PDF Viewer&#39;</span><span class="p">,</span> <span class="ss">filename</span><span class="p">:</span> <span class="s1">&#39;mhjfbmdgcfjbbpaeojofohoefgiehjai&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s1">&#39;Native Client&#39;</span><span class="p">,</span> <span class="ss">filename</span><span class="p">:</span> <span class="s1">&#39;internal-nacl-plugin&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Simula</span> <span class="no">WebGL</span> <span class="n">renderer</span> <span class="n">real</span>
</span></span><span class="line"><span class="cl">    <span class="n">const</span> <span class="n">getParameter</span> <span class="o">=</span> <span class="no">WebGLRenderingContext</span><span class="o">.</span><span class="n">prototype</span><span class="o">.</span><span class="n">getParameter</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="no">WebGLRenderingContext</span><span class="o">.</span><span class="n">prototype</span><span class="o">.</span><span class="n">getParameter</span> <span class="o">=</span> <span class="n">function</span><span class="p">(</span><span class="nb">p</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nb">p</span> <span class="o">===</span> <span class="mi">37445</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;Google Inc. (NVIDIA)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nb">p</span> <span class="o">===</span> <span class="mi">37446</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;ANGLE (NVIDIA, GeForce GTX 1080 ...)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="n">getParameter</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">this</span><span class="p">,</span> <span class="nb">p</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Client</span> <span class="no">Hints</span> <span class="no">API</span> <span class="p">(</span><span class="n">ausente</span> <span class="n">em</span> <span class="n">headless</span> <span class="o">=</span> <span class="n">bot</span> <span class="n">signal</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">navigator</span><span class="o">.</span><span class="n">userAgentData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="no">Object</span><span class="o">.</span><span class="n">defineProperty</span><span class="p">(</span><span class="n">navigator</span><span class="p">,</span> <span class="s1">&#39;userAgentData&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="ss">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">          <span class="ss">brands</span><span class="p">:</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span> <span class="ss">brand</span><span class="p">:</span> <span class="s1">&#39;Chromium&#39;</span><span class="p">,</span> <span class="ss">version</span><span class="p">:</span> <span class="s1">&#39;131&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span> <span class="ss">brand</span><span class="p">:</span> <span class="s1">&#39;Google Chrome&#39;</span><span class="p">,</span> <span class="ss">version</span><span class="p">:</span> <span class="s1">&#39;131&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="ss">mobile</span><span class="p">:</span> <span class="kp">false</span><span class="p">,</span> <span class="ss">platform</span><span class="p">:</span> <span class="s1">&#39;Linux&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="no">JS</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="nb">name</span><span class="p">:,</span> <span class="ss">wait_for</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">browser</span> <span class="o">=</span> <span class="no">Ferrum</span><span class="o">::</span><span class="no">Browser</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">headless</span><span class="p">:</span> <span class="s2">&#34;new&#34;</span><span class="p">,</span> <span class="ss">timeout</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">browser_options</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;disable-blink-features&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;AutomationControlled&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;user-agent&#34;</span> <span class="o">=&gt;</span> <span class="no">REAL_USER_AGENT</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Injeta ANTES de qualquer script da página rodar</span>
</span></span><span class="line"><span class="cl">    <span class="n">browser</span><span class="o">.</span><span class="n">page</span><span class="o">.</span><span class="n">command</span><span class="p">(</span><span class="s2">&#34;Page.addScriptToEvaluateOnNewDocument&#34;</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="no">STEALTH_JS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">browser</span><span class="o">.</span><span class="n">go_to</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">yield</span> <span class="n">browser</span>
</span></span><span class="line"><span class="cl">  <span class="k">ensure</span>
</span></span><span class="line"><span class="cl">    <span class="n">browser</span><span class="o">&amp;.</span><span class="n">quit</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O truque-chave é <code>Page.addScriptToEvaluateOnNewDocument</code> — um comando CDP que registra o JavaScript pra ser executado <strong>antes</strong> de qualquer script da página. Isso significa que, quando o bot detector roda, ele vê os valores falsos que injetamos, e não os reais. E <code>disable-blink-features=AutomationControlled</code> remove o flag <code>navigator.webdriver</code> no nível do browser, antes mesmo do JavaScript.</p>
<p>Isso funciona pra maioria dos sites. Morningstar, Hindenburg Research, sites de notícias genéricos — passam com o stealth. Yahoo Finance precisa de mais — TLS fingerprinting que bloqueia até o Chromium sem crumb authentication. Mas o browser stealth resolve o grosso.</p>
<h2>O Terceiro Obstáculo: DataDome e CDPs de Anti-Bot<span class="hx:absolute hx:-mt-20" id="o-terceiro-obstáculo-datadome-e-cdps-de-anti-bot"></span>
    <a href="#o-terceiro-obst%c3%a1culo-datadome-e-cdps-de-anti-bot" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>E aí chegamos aos profissionais. <strong>DataDome</strong>, Cloudflare Turnstile, PerimeterX, Akamai Bot Manager — são serviços especializados em detectar automação. E eles jogam num nível completamente diferente.</p>
<p>A Reuters, por exemplo, usa DataDome. Quando tentei acessar <code>reuters.com/world/</code> com o StealthBrowser, recebi de volta uma página de 1.500 bytes com um captcha JavaScript. Todos os patches de <code>navigator.webdriver</code>, WebGL, plugins — inúteis.</p>
<p>Por quê? Porque o DataDome não checa só propriedades JavaScript. Ele detecta o <strong>próprio protocolo de automação</strong>.</p>
<h3>O Problema Fundamental do CDP<span class="hx:absolute hx:-mt-20" id="o-problema-fundamental-do-cdp"></span>
    <a href="#o-problema-fundamental-do-cdp" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando o <strong>Ferrum</strong> (ou Puppeteer, ou Playwright) controla um Chrome, ele faz isso via Chrome DevTools Protocol — o CDP. E a primeira coisa que o Ferrum faz ao conectar é enviar <code>Runtime.enable</code> — um comando que ativa o runtime evaluation no browser.</p>
<p>O problema é que <code>Runtime.enable</code> muda o comportamento observável do browser. Especificamente, ele ativa interceptors internos que podem ser detectados via <code>console.debug</code>. O DataDome usa isso: ele faz um <code>console.debug</code> com um getter que observa se o runtime está interceptando. Se está — e com CDP sempre está — bot detectado.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// O que o DataDome faz (simplificado):
</span></span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">detected</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nb">Object</span><span class="p">.</span><span class="nx">defineProperty</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="s1">&#39;id&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">get</span><span class="o">:</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">detected</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="k">return</span> <span class="s1">&#39;test&#39;</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">debug</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// Se Runtime.enable está ativo, o getter é trigado
</span></span></span><span class="line"><span class="cl"><span class="c1">// Em um browser normal, não é
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso não é algo que você pode patchar com JavaScript. O <code>Runtime.enable</code> é mandado pelo Ferrum <strong>antes</strong> de qualquer script rodar — é parte do protocolo de automação. Pra resolver, você precisaria modificar o Ferrum pra não mandar esse comando, o que quebraria a maioria das funcionalidades da gem.</p>
<h3>As Alternativas (e Por Que Nenhuma é Ideal)<span class="hx:absolute hx:-mt-20" id="as-alternativas-e-por-que-nenhuma-é-ideal"></span>
    <a href="#as-alternativas-e-por-que-nenhuma-%c3%a9-ideal" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pesquisei extensivamente. As opções:</p>
<ol>
<li>
<p><strong>nodriver (Python)</strong>: Usa Chrome via CDP sem <code>Runtime.enable</code>. Não tem equivalente em Ruby. E mesmo assim, está numa corrida de gato-e-rato com DataDome.</p>
</li>
<li>
<p><strong>curl-impersonate</strong>: Faz TLS fingerprinting idêntico ao Chrome sem abrir um browser. Bom pra sites com TLS detection, inútil pra SPAs.</p>
</li>
<li>
<p><strong>Scraping APIs</strong> (ScraperAPI, ZenRows): Terceirizam o problema. Funcionam, mas custam dinheiro e adicionam latência e dependência.</p>
</li>
<li>
<p><strong>Fork do Ferrum</strong>: Modificar a gem pra opcionalmente não mandar <code>Runtime.enable</code>. Viável tecnicamente, mas manutenção infinita.</p>
</li>
<li>
<p><strong>Google News RSS</strong>: O Google já leu a Reuters. O resultado tá num feed RSS público. Sem JavaScript, sem DataDome, sem browser.</p>
</li>
</ol>
<p>A opção 5 venceu. Pra Reuters, troquei toda a complexidade de headless browser por:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ReutersClient</span>
</span></span><span class="line"><span class="cl">  <span class="no">RSS_URL</span> <span class="o">=</span> <span class="s2">&#34;https://news.google.com/rss/search?q=when:24h+allinurl:reuters.com/world&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch_headlines</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">xml</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="no">URI</span><span class="p">(</span><span class="no">RSS_URL</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">doc</span> <span class="o">=</span> <span class="no">REXML</span><span class="o">::</span><span class="no">Document</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">xml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">doc</span><span class="o">.</span><span class="n">get_elements</span><span class="p">(</span><span class="s2">&#34;//item&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="n">limit</span><span class="p">)</span><span class="o">.</span><span class="n">filter_map</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">title</span> <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">to_s</span><span class="o">&amp;.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">      <span class="n">url</span>   <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;link&#34;</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">to_s</span><span class="o">&amp;.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">      <span class="k">next</span> <span class="k">if</span> <span class="n">title</span><span class="o">.</span><span class="n">blank?</span> <span class="o">||</span> <span class="n">url</span><span class="o">.</span><span class="n">blank?</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span> <span class="ss">title</span><span class="p">:</span> <span class="n">title</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\s*-\s*Reuters\s*\z/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">),</span> <span class="ss">url</span><span class="p">:</span> <span class="n">url</span><span class="p">,</span> <span class="ss">category</span><span class="p">:</span> <span class="kp">nil</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;ReutersClient failed: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">[]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Simples. Confiável. Zero dependências de browser. O link do Google News redireciona pro artigo original quando o leitor clica — funciona perfeitamente pra uma newsletter.</p>
<p>Nem todo problema de scraping precisa de solução de scraping. Às vezes o dado já está disponível num formato acessível — você só precisa achar.</p>
<h2>O Quarto Obstáculo: Sites que Mudam de Estrutura (Diariamente)<span class="hx:absolute hx:-mt-20" id="o-quarto-obstáculo-sites-que-mudam-de-estrutura-diariamente"></span>
    <a href="#o-quarto-obst%c3%a1culo-sites-que-mudam-de-estrutura-diariamente" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Mesmo quando o scraping funciona, a manutenção nunca para. O Morningstar é um caso didático — porque me fez errar <strong>duas vezes</strong> antes de chegar numa solução resiliente.</p>
<h3>Tentativa 1: Selecionar pela Hierarquia DOM<span class="hx:absolute hx:-mt-20" id="tentativa-1-selecionar-pela-hierarquia-dom"></span>
    <a href="#tentativa-1-selecionar-pela-hierarquia-dom" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Originalmente, os links de artigos estavam dentro de <code>&lt;h2&gt;</code> e <code>&lt;h3&gt;</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/markets/article-slug&#34;</span><span class="p">&gt;</span>Título do Artigo<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Um dia depois: zero resultados. O Morningstar mudou a estrutura pra flat links com classes CSS específicas — sem heading elements:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;mdc-basic-feed-item__mdc&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/markets/article-slug&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">header</span><span class="p">&gt;&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>Título do Artigo<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;&lt;/</span><span class="nt">header</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;mdc-metadata__mdc&#34;</span><span class="p">&gt;</span>Autor · Data<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Tentativa 2: Selecionar pela Classe CSS<span class="hx:absolute hx:-mt-20" id="tentativa-2-selecionar-pela-classe-css"></span>
    <a href="#tentativa-2-selecionar-pela-classe-css" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A correção óbvia: trocar hierarquia DOM por seletores CSS baseados na classe do componente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">ARTICLE_CSS</span> <span class="o">=</span> <span class="s2">&#34;a.mdc-basic-feed-item__mdc, a.mdc-card--link__mdc&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="no">ARTICLE_CSS</span><span class="p">)</span><span class="o">.</span><span class="n">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">link</span><span class="o">|</span> <span class="o">...</span> <span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Funcionou. Por exatamente um dia. Depois: zero resultados de novo. O Morningstar — como muitos sites modernos com frameworks CSS-in-JS — gera nomes de classe dinamicamente. O que era <code>mdc-basic-feed-item__mdc</code> virou outra coisa. E pode virar algo diferente amanhã.</p>
<p>Isso é cada vez mais comum. Sites que usam CSS Modules, styled-components, ou Tailwind com JIT geram hashes aleatórios nos nomes de classe como parte do build. A cada deploy do site, as classes mudam. Seu seletor hardcoded é uma bomba-relógio.</p>
<h3>Tentativa 3: Não Depender de Nenhum Nome de Classe<span class="hx:absolute hx:-mt-20" id="tentativa-3-não-depender-de-nenhum-nome-de-classe"></span>
    <a href="#tentativa-3-n%c3%a3o-depender-de-nenhum-nome-de-classe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A solução final é inverter a lógica: ao invés de dizer ao parser <strong>quais</strong> classes procurar, deixar o parser <strong>descobrir</strong> quais links são artigos baseado em propriedades estruturais que não mudam:</p>
<ol>
<li><strong>Links de artigos têm URLs longas</strong>: <code>/news/why-stock-market-is-up-today-2026-02-17</code> vs links de navegação como <code>/markets</code> ou <code>/tools</code></li>
<li><strong>Links de artigos são agrupados</strong>: aparecem em listas, feeds, grids — múltiplos links com a mesma classe CSS</li>
<li><strong>Links de artigos têm títulos descritivos</strong>: textos longos, não labels curtas como &ldquo;Markets&rdquo; ou &ldquo;Tools&rdquo;</li>
</ol>
<p>O algoritmo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">parse_html</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="n">limit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">doc</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 1. Coleta todos os &lt;a&gt; internos com href, classe, e texto suficiente</span>
</span></span><span class="line"><span class="cl">  <span class="n">candidates</span> <span class="o">=</span> <span class="n">doc</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="s2">&#34;a[href]&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">filter_map</span> <span class="k">do</span> <span class="o">|</span><span class="n">link</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">href</span> <span class="o">=</span> <span class="n">link</span><span class="o">[</span><span class="s2">&#34;href&#34;</span><span class="o">].</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">    <span class="n">css_class</span> <span class="o">=</span> <span class="n">link</span><span class="o">[</span><span class="s2">&#34;class&#34;</span><span class="o">].</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">href</span><span class="o">.</span><span class="n">blank?</span> <span class="o">||</span> <span class="n">css_class</span><span class="o">.</span><span class="n">blank?</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">unless</span> <span class="n">internal_href?</span><span class="p">(</span><span class="n">href</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="n">extract_path</span><span class="p">(</span><span class="n">href</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">segments</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:empty?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">segments</span><span class="o">.</span><span class="n">length</span> <span class="o">&lt;</span> <span class="mi">2</span>  <span class="c1"># links de nav têm 1 segmento: /markets</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="n">extract_title</span><span class="p">(</span><span class="n">link</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">title</span><span class="o">.</span><span class="n">blank?</span> <span class="o">||</span> <span class="n">title</span><span class="o">.</span><span class="n">length</span> <span class="o">&lt;</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="ss">title</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span> <span class="ss">url</span><span class="p">:</span> <span class="n">absolutize</span><span class="p">(</span><span class="n">href</span><span class="p">),</span> <span class="ss">css_class</span><span class="p">:</span> <span class="n">css_class</span><span class="p">,</span> <span class="ss">slug_length</span><span class="p">:</span> <span class="n">path</span><span class="o">.</span><span class="n">length</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 2. Agrupa por classe CSS</span>
</span></span><span class="line"><span class="cl">  <span class="n">groups</span> <span class="o">=</span> <span class="n">candidates</span><span class="o">.</span><span class="n">group_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="o">[</span><span class="ss">:css_class</span><span class="o">]</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. Filtra: mínimo 3 links no grupo (clusters de artigos, não links soltos)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 4. Seleciona o grupo com maior média de comprimento de slug</span>
</span></span><span class="line"><span class="cl">  <span class="n">best</span> <span class="o">=</span> <span class="n">groups</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">_cls</span><span class="p">,</span> <span class="n">items</span><span class="o">|</span> <span class="n">items</span><span class="o">.</span><span class="n">length</span> <span class="o">&gt;=</span> <span class="mi">3</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">max_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">_cls</span><span class="p">,</span> <span class="n">items</span><span class="o">|</span> <span class="n">items</span><span class="o">.</span><span class="n">sum</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="n">i</span><span class="o">[</span><span class="ss">:slug_length</span><span class="o">]</span> <span class="p">}</span><span class="o">.</span><span class="n">to_f</span> <span class="o">/</span> <span class="n">items</span><span class="o">.</span><span class="n">length</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">[]</span> <span class="k">unless</span> <span class="n">best</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">best</span><span class="o">[</span><span class="mi">1</span><span class="o">].</span><span class="n">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="o">.</span><span class="n">slice</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="ss">:url</span><span class="p">)</span> <span class="p">}</span><span class="o">.</span><span class="n">uniq</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="o">|</span> <span class="n">a</span><span class="o">[</span><span class="ss">:url</span><span class="o">]</span> <span class="p">}</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="n">limit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O que salva essa abordagem: <strong>nenhum nome de classe é hardcoded</strong>. O Morningstar pode renomear todas as classes amanhã e o parser vai continuar funcionando — porque ele descobre o padrão de artigos pela estrutura, não pelo nome.</p>
<p>O que é estável num site:</p>
<ul>
<li><strong>URLs de artigos são sempre mais longas</strong> que URLs de navegação (isso é arquitetural, não cosmético)</li>
<li><strong>Artigos são sempre agrupados</strong> sob o mesmo componente (isso é UX, não implementação)</li>
<li><strong>Títulos de artigos são descritivos</strong> (isso é editorial, não técnico)</li>
</ul>
<p>O que <strong>não</strong> é estável:</p>
<ul>
<li>Nomes de classes CSS (mudam a cada build)</li>
<li>Hierarquia DOM (muda a cada redesign)</li>
<li>Atributos <code>data-*</code> (mudam conforme a equipe de frontend decide)</li>
</ul>
<p>Resumindo: não tente identificar elementos pelo nome — identifique pelo comportamento. Links de artigos se comportam diferente de links de navegação de formas mensuráveis (URL length, clustering, título), e essas propriedades são inerentes ao que o elemento É, não a como ele foi implementado.</p>
<h2>A Hierarquia de Confiabilidade<span class="hx:absolute hx:-mt-20" id="a-hierarquia-de-confiabilidade"></span>
    <a href="#a-hierarquia-de-confiabilidade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois de horas quebrando a cabeça, ficou óbvia uma hierarquia de fontes de dados:</p>
<table>
  <thead>
      <tr>
          <th>Fonte</th>
          <th>Confiabilidade</th>
          <th>Latência</th>
          <th>Manutenção</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API oficial (JSON)</td>
          <td>Alta</td>
          <td>~200ms</td>
          <td>Baixa</td>
      </tr>
      <tr>
          <td>RSS/Atom feed</td>
          <td>Alta</td>
          <td>~300ms</td>
          <td>Baixa</td>
      </tr>
      <tr>
          <td>HTML server-rendered</td>
          <td>Média</td>
          <td>~500ms</td>
          <td>Média</td>
      </tr>
      <tr>
          <td>SPA com headless browser</td>
          <td>Baixa-Média</td>
          <td>~5s</td>
          <td>Alta</td>
      </tr>
      <tr>
          <td>Site com anti-bot ativo</td>
          <td>Baixa</td>
          <td>~8s+</td>
          <td>Altíssima</td>
      </tr>
  </tbody>
</table>
<p>A regra: <strong>sempre prefira a fonte mais simples que funcione</strong>. Se o site tem RSS, use RSS. Se tem API, use a API. Só vá pro headless browser quando não há alternativa.</p>
<p>No projeto, a distribuição ficou assim:</p>
<p><strong>HTTP simples</strong> (Net::HTTP ou HTTParty):</p>
<ul>
<li>Google News RSS (Reuters world headlines)</li>
<li>Hacker News RSS (top stories)</li>
<li>HackerNoon RSS (top stories)</li>
<li>dev.to API (trending articles)</li>
<li>Jikan API (anime rankings)</li>
<li>YouTube Data API (vídeos)</li>
<li>Yahoo Finance news RSS</li>
</ul>
<p><strong>Headless browser</strong> (StealthBrowser + Ferrum):</p>
<ul>
<li>Morningstar (SPA + componentes dinâmicos)</li>
<li>Yahoo Finance charts (TLS fingerprinting + crumb auth)</li>
<li>FinViz market map (canvas rendering)</li>
<li>Artigos genéricos de leitores (qualquer URL pode ser SPA)</li>
<li>Hindenburg Research (JavaScript-rendered)</li>
</ul>
<p>Ou seja: <strong>a maioria das fontes de dados funciona sem browser</strong>. O browser é necessário pra meia dúzia de sites problemáticos — mas são esses sites que dão mais trabalho.</p>
<h2>O Custo Real<span class="hx:absolute hx:-mt-20" id="o-custo-real"></span>
    <a href="#o-custo-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos ser práticos sobre o que &ldquo;empacotar um Chrome&rdquo; significa:</p>
<p><strong>Imagem Docker</strong>: +300MB. O Chromium e suas dependências (fontes, libs gráficas, sandbox) são enormes. Num Dockerfile otimizado com multi-stage build:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Instala Chromium e dependências mínimas</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y chromium fonts-liberation libnss3 libatk-bridge2.0-0 <span class="se">\
</span></span></span><span class="line"><span class="cl">    libdrm2 libxkbcommon0 libgbm1</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Memória</strong>: Cada instância de Chromium consome 200-400MB de RAM. Se você tem 3 workers paralelos, são 1.2GB só pra browsers. Num VPS de 2GB, isso é 60% da memória.</p>
<p><strong>Latência</strong>: Uma página que leva 200ms via HTTP leva 3-8 segundos via headless browser (boot do processo, navegação, JS execution, network idle). Pra 10 fontes de dados, são 30-80 segundos ao invés de 2.</p>
<p><strong>Estabilidade</strong>: Chromium pode crashar, travar, ou vazar memória. Timeout de 30 segundos com cleanup no <code>ensure</code> é obrigatório:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="nb">name</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">  <span class="n">browser</span> <span class="o">=</span> <span class="n">create</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="nb">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">browser</span><span class="o">.</span><span class="n">go_to</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">yield</span> <span class="n">browser</span>
</span></span><span class="line"><span class="cl"><span class="k">ensure</span>
</span></span><span class="line"><span class="cl">  <span class="n">browser</span><span class="o">&amp;.</span><span class="n">quit</span>  <span class="c1"># SEMPRE quit, mesmo com exceção</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se não fizer o <code>quit</code>, o processo Chromium fica pendurado consumindo CPU e memória. Já tive servidor com 15 processos Chrome zumbis comendo 3GB de RAM porque um <code>ensure</code> estava faltando.</p>
<h2>O Futuro (Ou: Vai Piorar)<span class="hx:absolute hx:-mt-20" id="o-futuro-ou-vai-piorar"></span>
    <a href="#o-futuro-ou-vai-piorar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A tendência é clara: sites estão ficando mais difíceis de acessar programaticamente. A cada trimestre, os DataDomes e Cloudflares da vida adicionam novas técnicas de detecção. Chrome headless fica mais parecido com Chrome real — mas os detectores acompanham.</p>
<p>A web que nasceu aberta e legível por máquina está se tornando um jardim murado. Até RSS, que é trivial de implementar e útil pra todo mundo, está sendo abandonado por muitos sites — porque não querem que seus dados sejam lidos fora do contexto (e dos anúncios) do site original.</p>
<p>Pra quem constrói sistemas que agregam informação, o playbook é:</p>
<ol>
<li><strong>Use APIs e feeds sempre que existirem</strong>. É mais estável, mais rápido, mais respeitoso</li>
<li><strong>Mantenha um browser stealth pra SPAs</strong> que não têm alternativa</li>
<li><strong>Nunca hardcode seletores CSS</strong>. Sites com CSS-in-JS mudam nomes de classe a cada deploy. Identifique elementos por propriedades estruturais (comprimento de URL, clustering, tamanho do título) que sobrevivem a redesigns</li>
<li><strong>Detecte bloqueios e falhe graciosamente</strong> — <code>og:description</code> como fallback, título extraído da URL como último recurso</li>
<li><strong>Nunca dependa de um único método de acesso</strong>. O site que funciona hoje pode bloquear amanhã</li>
<li><strong>Aceite que alguns sites são inacessíveis</strong>. Se o DataDome não deixa, o dado terá que vir de outra fonte</li>
</ol>
<p>A web em 2026 é um campo de batalha entre quem quer ler e quem quer controlar a leitura. Mas no longo prazo, a criatividade de quem quer acessar dados vence — mas custa mais esforço a cada ano. E, diferente do que eu pensei no começo: nunca é simples.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>desenvolvimento-web</category><category>automacao</category></item><item><title>AI Agents: Instalando LSPs pro Crush</title><link>https://akitaonrails.github.io/2026/01/21/ai-agents-instalando-lsps-pro-crush/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/01/21/ai-agents-instalando-lsps-pro-crush/</guid><pubDate>Wed, 21 Jan 2026 13:14:07 -0300</pubDate><description>&lt;p&gt;Este post é mais pra eu documentar e não esquecer. Como falei nos últimos posts, tenho usado &lt;a href="https://github.com/charmbracelet/crush"target="_blank" rel="noopener"&gt;Charm&amp;rsquo;s Crush&lt;/a&gt; como meu agente favorito de IA pra programação. Repetindo: tanto faz se você usa Claude Code ou OpenCode, todos eles são igualmente bons. Minha preferência pessoal ainda é Crush porque acho mais bonito, só isso.&lt;/p&gt;
&lt;p&gt;Ainda não entendi o quanto faz diferença ou não, mas tanto OpenCode quanto Crush tem suporte a Language Servers (LSPs) então achei legal deixar configurado. Eis meu &lt;code&gt;~/.config/crush/crush.json&lt;/code&gt;:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é mais pra eu documentar e não esquecer. Como falei nos últimos posts, tenho usado <a href="https://github.com/charmbracelet/crush"target="_blank" rel="noopener">Charm&rsquo;s Crush</a> como meu agente favorito de IA pra programação. Repetindo: tanto faz se você usa Claude Code ou OpenCode, todos eles são igualmente bons. Minha preferência pessoal ainda é Crush porque acho mais bonito, só isso.</p>
<p>Ainda não entendi o quanto faz diferença ou não, mas tanto OpenCode quanto Crush tem suporte a Language Servers (LSPs) então achei legal deixar configurado. Eis meu <code>~/.config/crush/crush.json</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;$schema&#34;</span><span class="p">:</span> <span class="s2">&#34;https://charm.land/crush.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;default_provider&#34;</span><span class="p">:</span> <span class="s2">&#34;openrouter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;providers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;openrouter&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;api_key&#34;</span><span class="p">:</span> <span class="s2">&#34;sk-...&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;lmstudio&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;LM Studio&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;base_url&#34;</span><span class="p">:</span> <span class="s2">&#34;http://localhost:1234/v1/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;openai&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;models&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;gpt-oss-120b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;gpt-oss:120b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">100000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;gpt-oss-20b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;gpt-oss:20b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">130000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;zai-org/glm-4.6v-flash&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;glm-4.6v-flash&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">130000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;deepseek-coder-v2-lite-instruct&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;deepseek-coder-v2-lite-instruct&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">64000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen/qwen3-coder-30b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen3-coder-30b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">120000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;ollama&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;openai-compat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;base_url&#34;</span><span class="p">:</span> <span class="s2">&#34;http://127.0.0.1:11434/v1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;api_key&#34;</span><span class="p">:</span> <span class="s2">&#34;ollama&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;models&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;glm-4.7-flash&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;glm-4.7-flash&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">65536</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;gpt-oss-20b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;gpt-oss:20b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">64000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen3-nuclear&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen3-nuclear&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">120000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen3-coder:30b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen3-coder:30b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">120000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen2.5-coder:32b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;qwen2.5-coder:32b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">32768</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;deepseek-coder-v2:16b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;deepseek-coder-v2:16b&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">128000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;glm-4.7:cloud&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;glm-4.7:cloud&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;context_window&#34;</span><span class="p">:</span> <span class="mi">128000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;default_max_tokens&#34;</span><span class="p">:</span> <span class="mi">-1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;supports_tools&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;lsp&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;gopls&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;gopls&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;rust-analyzer&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;rust-analyzer&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;ruby-lsp&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;ruby-lsp&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;solargraph&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;solargraph&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;pylsp&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;pylsp&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;pyright&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;pyright-langserver&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;elixir-ls&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;elixir-ls&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;docker-langserver&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;docker-langserver&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;languages&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;dockerfile&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;bash-language-server&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;bash-language-server&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;start&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;marksman&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;marksman&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;server&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;typescript-language-server&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;typescript-language-server&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;vscode-html-language-server&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;vscode-html-language-server&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;vscode-css-language-server&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;vscode-css-language-server&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;vscode-json-language-server&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;vscode-json-language-server&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;yaml-language-server&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;yaml-language-server&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;--stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;taplo&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;taplo&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;lsp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;stdio&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;mcp&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;blender&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;stdio&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;uvx&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;blender-mcp&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notem que eu uso primariamente Claude Opus 4.5 via OpenRouter e tenho LM Studio e Ollama configurados como providers pra testar outros modelos open source.</p>
<p>Também uso <a href="/2025/09/07/omarchy-2-0-mise-pra-organizar-ambientes-de-desenvolvimento/">Mise</a> pra organizar as linguagens de programação que uso.</p>
<p>Porém, agora precisa instalar os LSPs de cada linguagem. Eu não conheço um jeito melhor de gerenciar isso da mesma forma como com um Mason no Neovim. Então fiz um script pra instalar tudo pra mim (no meu Arch Linux). Pra isso criei este script <code>~/.local/bin/install-lsps.sh</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># LSP Installer for Arch Linux with Mise</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Installs major LSPs using native package managers or yay</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Only installs LSPs for languages that are already installed</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">GREEN</span><span class="o">=</span><span class="s1">&#39;\033[0;32m&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">BLUE</span><span class="o">=</span><span class="s1">&#39;\033[0;34m&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">YELLOW</span><span class="o">=</span><span class="s1">&#39;\033[1;33m&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">RED</span><span class="o">=</span><span class="s1">&#39;\033[0;31m&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">NC</span><span class="o">=</span><span class="s1">&#39;\033[0m&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">info<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> -e <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BLUE</span><span class="si">}</span><span class="s2">[INFO]</span><span class="si">${</span><span class="nv">NC</span><span class="si">}</span><span class="s2"> </span><span class="nv">$1</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">success<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> -e <span class="s2">&#34;</span><span class="si">${</span><span class="nv">GREEN</span><span class="si">}</span><span class="s2">[OK]</span><span class="si">${</span><span class="nv">NC</span><span class="si">}</span><span class="s2"> </span><span class="nv">$1</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">warn<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> -e <span class="s2">&#34;</span><span class="si">${</span><span class="nv">YELLOW</span><span class="si">}</span><span class="s2">[SKIP]</span><span class="si">${</span><span class="nv">NC</span><span class="si">}</span><span class="s2"> </span><span class="nv">$1</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">skip_lang<span class="o">()</span> <span class="o">{</span> <span class="nb">echo</span> -e <span class="s2">&#34;</span><span class="si">${</span><span class="nv">RED</span><span class="si">}</span><span class="s2">[LANG]</span><span class="si">${</span><span class="nv">NC</span><span class="si">}</span><span class="s2"> </span><span class="nv">$1</span><span class="s2"> not installed, skipping LSPs&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">command_exists<span class="o">()</span> <span class="o">{</span> <span class="nb">command</span> -v <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Language detection helpers</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">has_c<span class="o">()</span>       <span class="o">{</span> command_exists gcc <span class="o">||</span> command_exists clang<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_rust<span class="o">()</span>    <span class="o">{</span> command_exists rustc <span class="o">||</span> command_exists cargo<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_go<span class="o">()</span>      <span class="o">{</span> command_exists go<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_python<span class="o">()</span>  <span class="o">{</span> command_exists python <span class="o">||</span> command_exists python3<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_node<span class="o">()</span>    <span class="o">{</span> command_exists node <span class="o">||</span> command_exists npm<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_java<span class="o">()</span>    <span class="o">{</span> command_exists java <span class="o">||</span> command_exists javac<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_kotlin<span class="o">()</span>  <span class="o">{</span> command_exists kotlin <span class="o">||</span> command_exists kotlinc<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_ruby<span class="o">()</span>    <span class="o">{</span> command_exists ruby<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_php<span class="o">()</span>     <span class="o">{</span> command_exists php<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_lua<span class="o">()</span>     <span class="o">{</span> command_exists lua <span class="o">||</span> command_exists luajit<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_zig<span class="o">()</span>     <span class="o">{</span> command_exists zig<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_haskell<span class="o">()</span> <span class="o">{</span> command_exists ghc <span class="o">||</span> command_exists stack <span class="o">||</span> command_exists cabal<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_elixir<span class="o">()</span>  <span class="o">{</span> command_exists elixir <span class="o">||</span> command_exists iex<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_scala<span class="o">()</span>   <span class="o">{</span> command_exists scala <span class="o">||</span> command_exists sbt<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_dotnet<span class="o">()</span>  <span class="o">{</span> command_exists dotnet<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_ocaml<span class="o">()</span>   <span class="o">{</span> command_exists ocaml <span class="o">||</span> command_exists opam<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_clojure<span class="o">()</span> <span class="o">{</span> command_exists clojure <span class="o">||</span> command_exists clj <span class="o">||</span> command_exists lein<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_nix<span class="o">()</span>     <span class="o">{</span> command_exists nix<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_terraform<span class="o">()</span> <span class="o">{</span> command_exists terraform <span class="o">||</span> command_exists tofu<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_latex<span class="o">()</span>   <span class="o">{</span> command_exists latex <span class="o">||</span> command_exists pdflatex <span class="o">||</span> command_exists xelatex<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_docker<span class="o">()</span>  <span class="o">{</span> command_exists docker <span class="o">||</span> command_exists podman<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_ansible<span class="o">()</span> <span class="o">{</span> command_exists ansible<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_sql<span class="o">()</span>     <span class="o">{</span> command_exists psql <span class="o">||</span> command_exists mysql <span class="o">||</span> command_exists sqlite3<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">has_bash<span class="o">()</span>    <span class="o">{</span> command_exists bash<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Package installation helpers</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_pacman<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">pkg</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> pacman -Qi <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2"> already installed&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        sudo pacman -S --noconfirm --needed <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> success <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed: </span><span class="nv">$pkg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">install_yay<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">pkg</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! command_exists yay<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;yay not found, skipping </span><span class="nv">$pkg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> yay -Qi <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2"> already installed&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        yay -S --noconfirm --needed <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> success <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed: </span><span class="nv">$pkg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">install_npm<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">pkg</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> npm list -g <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="p">&amp;</span>&gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2"> already installed&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        npm install -g <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> success <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed: </span><span class="nv">$pkg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">install_pipx<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">pkg</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! command_exists pipx<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        pip install --user pipx 2&gt;/dev/null <span class="o">||</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">            pip install --user <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> success <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed: </span><span class="nv">$pkg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="cl">        <span class="o">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> pipx list 2&gt;/dev/null <span class="p">|</span> grep -q <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2"> already installed&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        pipx install <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> success <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed: </span><span class="nv">$pkg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">install_go<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">pkg</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">name</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">pkg</span><span class="p">##*/</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">name</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">name</span><span class="p">%%@*</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Check if binary already exists in GOPATH/bin</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">gobin</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">GOBIN</span><span class="k">:-</span><span class="si">${</span><span class="nv">GOPATH</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/go</span><span class="si">}</span><span class="p">/bin</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[[</span> -x <span class="s2">&#34;</span><span class="nv">$gobin</span><span class="s2">/</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2"> already installed&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    go install <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> success <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed: </span><span class="nv">$name</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">install_gem<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">pkg</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> gem list -i <span class="s2">&#34;^</span><span class="si">${</span><span class="nv">pkg</span><span class="si">}</span>$<span class="s2">&#34;</span> <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2"> already installed&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        gem install <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> success <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed: </span><span class="nv">$pkg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># C/C++ LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_c_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_c<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;C/C++ (gcc/clang)&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing C/C++ LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman clang  <span class="c1"># includes clangd</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Rust LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_rust_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_rust<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Rust&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Rust LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> command_exists rustup<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        rustup component add rust-analyzer 2&gt;/dev/null <span class="o">&amp;&amp;</span> success <span class="s2">&#34;rust-analyzer (rustup)&#34;</span> <span class="o">||</span> install_pacman rust-analyzer
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        install_pacman rust-analyzer
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Go LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_go_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_go<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Go&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Go LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    install_go golang.org/x/tools/gopls@latest
</span></span><span class="line"><span class="cl">    install_go github.com/go-delve/delve/cmd/dlv@latest
</span></span><span class="line"><span class="cl">    install_go github.com/nametake/golangci-lint-langserver@latest
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Python LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_python_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_python<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Python&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Python LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Prefer npm pyright (faster updates) if node available</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> has_node<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        install_npm pyright
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        install_pipx pyright
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    install_pipx python-lsp-server
</span></span><span class="line"><span class="cl">    install_pipx ruff-lsp
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Node.js/TypeScript LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_node_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_node<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Node.js&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Node.js/TypeScript LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    install_npm typescript
</span></span><span class="line"><span class="cl">    install_npm typescript-language-server
</span></span><span class="line"><span class="cl">    install_npm vscode-langservers-extracted  <span class="c1"># HTML, CSS, JSON, ESLint</span>
</span></span><span class="line"><span class="cl">    install_npm <span class="s2">&#34;@tailwindcss/language-server&#34;</span>
</span></span><span class="line"><span class="cl">    install_npm <span class="s2">&#34;@vue/language-server&#34;</span>
</span></span><span class="line"><span class="cl">    install_npm svelte-language-server
</span></span><span class="line"><span class="cl">    install_npm graphql-language-service-cli
</span></span><span class="line"><span class="cl">    install_npm <span class="s2">&#34;@prisma/language-server&#34;</span>
</span></span><span class="line"><span class="cl">    install_npm emmet-ls
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Java LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_java_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_java<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Java&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Java LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay jdtls
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Kotlin LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_kotlin_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_kotlin<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Kotlin&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Kotlin LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay kotlin-language-server
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Ruby LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_ruby_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_ruby<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Ruby&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Ruby LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_gem solargraph
</span></span><span class="line"><span class="cl">    install_gem ruby-lsp
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># PHP LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_php_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_php<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;PHP&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_node<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;PHP LSP (intelephense) requires npm, skipping&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing PHP LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_npm intelephense
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Lua LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_lua_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_lua<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Lua&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Lua LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman lua-language-server
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Zig LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_zig_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_zig<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Zig&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Zig LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman zls
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Haskell LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_haskell_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_haskell<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Haskell&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Haskell LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay haskell-language-server
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Elixir LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_elixir_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_elixir<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Elixir&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Elixir LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay elixir-ls
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Scala LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_scala_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_scala<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Scala&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Scala LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay metals
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># C# / F# (.NET) LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_dotnet_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_dotnet<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;.NET (C#/F#)&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing .NET LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay omnisharp-roslyn
</span></span><span class="line"><span class="cl">    install_yay fsautocomplete
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># OCaml LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_ocaml_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_ocaml<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;OCaml&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing OCaml LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> command_exists opam<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        opam install ocaml-lsp-server -y <span class="o">&amp;&amp;</span> success <span class="s2">&#34;ocaml-lsp-server&#34;</span> <span class="o">||</span> warn <span class="s2">&#34;Failed&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        install_yay ocaml-lsp-server
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Clojure LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_clojure_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_clojure<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Clojure&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Clojure LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay clojure-lsp-bin
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Nix LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_nix_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_nix<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Nix&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Nix LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman nil
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Bash LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_bash_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_bash<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Bash&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Bash LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman bash-language-server
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Terraform LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_terraform_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_terraform<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Terraform&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Terraform LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman terraform-ls
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># LaTeX LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_latex_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_latex<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;LaTeX&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing LaTeX LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman texlab
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Docker LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_docker_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_docker<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Docker&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Docker LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_yay dockerfile-language-server
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Ansible LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_ansible_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_ansible<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;Ansible&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_node<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        warn <span class="s2">&#34;Ansible LSP requires npm, skipping&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing Ansible LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_npm <span class="s2">&#34;@ansible/ansible-language-server&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># SQL LSPs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_sql_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> ! has_sql<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        skip_lang <span class="s2">&#34;SQL (psql/mysql/sqlite)&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing SQL LSPs...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman sqls
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Generic config file LSPs (always install)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">install_generic_lsps<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    info <span class="s2">&#34;Installing generic config LSPs (YAML, TOML, JSON, Markdown, XML)...&#34;</span>
</span></span><span class="line"><span class="cl">    install_pacman yaml-language-server
</span></span><span class="line"><span class="cl">    install_pacman taplo               <span class="c1"># TOML</span>
</span></span><span class="line"><span class="cl">    install_pacman marksman            <span class="c1"># Markdown</span>
</span></span><span class="line"><span class="cl">    install_yay lemminx                <span class="c1"># XML</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Main</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ─────────────────────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">main<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;═══════════════════════════════════════════════════════════&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;         LSP Installer for Arch Linux + Mise&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;    (Only installs LSPs for detected languages)&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;═══════════════════════════════════════════════════════════&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Language-specific LSPs</span>
</span></span><span class="line"><span class="cl">    install_c_lsps
</span></span><span class="line"><span class="cl">    install_rust_lsps
</span></span><span class="line"><span class="cl">    install_go_lsps
</span></span><span class="line"><span class="cl">    install_python_lsps
</span></span><span class="line"><span class="cl">    install_node_lsps
</span></span><span class="line"><span class="cl">    install_java_lsps
</span></span><span class="line"><span class="cl">    install_kotlin_lsps
</span></span><span class="line"><span class="cl">    install_ruby_lsps
</span></span><span class="line"><span class="cl">    install_php_lsps
</span></span><span class="line"><span class="cl">    install_lua_lsps
</span></span><span class="line"><span class="cl">    install_zig_lsps
</span></span><span class="line"><span class="cl">    install_haskell_lsps
</span></span><span class="line"><span class="cl">    install_elixir_lsps
</span></span><span class="line"><span class="cl">    install_scala_lsps
</span></span><span class="line"><span class="cl">    install_dotnet_lsps
</span></span><span class="line"><span class="cl">    install_ocaml_lsps
</span></span><span class="line"><span class="cl">    install_clojure_lsps
</span></span><span class="line"><span class="cl">    install_nix_lsps
</span></span><span class="line"><span class="cl">    install_bash_lsps
</span></span><span class="line"><span class="cl">    install_terraform_lsps
</span></span><span class="line"><span class="cl">    install_latex_lsps
</span></span><span class="line"><span class="cl">    install_docker_lsps
</span></span><span class="line"><span class="cl">    install_ansible_lsps
</span></span><span class="line"><span class="cl">    install_sql_lsps
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Always-useful LSPs for config files</span>
</span></span><span class="line"><span class="cl">    install_generic_lsps
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;═══════════════════════════════════════════════════════════&#34;</span>
</span></span><span class="line"><span class="cl">    success <span class="s2">&#34;LSP installation complete!&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;═══════════════════════════════════════════════════════════&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">main <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Teoricamente é só isso. Se alguém conhecer um jeito melhor, mande nos comentários abaixo.</p>
]]></content:encoded><category>agentes-de-codigo</category><category>automacao</category><category>ferramentas-de-desenvolvimento</category></item><item><title>Omarchy - Não entrar em Sleep enquanto Copia</title><link>https://akitaonrails.github.io/2025/11/20/omarchy-no-sleep-on-copy/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/11/20/omarchy-no-sleep-on-copy/</guid><pubDate>Thu, 20 Nov 2025 14:00:00 -0300</pubDate><description>&lt;p&gt;Esta dica não é específica de Omarchy mas de Linux em geral, mas coloquei na categoria de Omarchy só pra ficar consistente no meu blog.&lt;/p&gt;
&lt;h3&gt;Introdução&lt;span class="hx:absolute hx:-mt-20" id="introdução"&gt;&lt;/span&gt;
&lt;a href="#introdu%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Quando eu me afasto do meu PC, o Hyprland vai usar as configurações em &lt;code&gt;~/.config/hypr/hypridle.conf&lt;/code&gt; pra saber em que timeout desligar o monitor e depois, quando colocar meu PC pra dormir (sleep). Podemos configurar o tempo que quisermos lá, como 10 min pra apagar a tela, 20 min sem atividade pra dormir.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Esta dica não é específica de Omarchy mas de Linux em geral, mas coloquei na categoria de Omarchy só pra ficar consistente no meu blog.</p>
<h3>Introdução<span class="hx:absolute hx:-mt-20" id="introdução"></span>
    <a href="#introdu%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando eu me afasto do meu PC, o Hyprland vai usar as configurações em <code>~/.config/hypr/hypridle.conf</code> pra saber em que timeout desligar o monitor e depois, quando colocar meu PC pra dormir (sleep). Podemos configurar o tempo que quisermos lá, como 10 min pra apagar a tela, 20 min sem atividade pra dormir.</p>
<p>Se tiver algum app que queira fazer alguma manutenção em background que demore mais que isso pra terminar, ele pode sinalizar pro sistema não dormir. Dá pra ver quais apps fazem isso usando o comando <code>systemd-inhibit --list</code>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20251120140935_screenshot-2025-11-20_14-09-09.png" alt="inhibit list"  loading="lazy" /></p>
<h3>O Problema<span class="hx:absolute hx:-mt-20" id="o-problema"></span>
    <a href="#o-problema" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Vira e mexe eu deixo copiando grandes quantidades de arquivos, por exemplo, do meu NAS pra um micro SD Card. Pode levar mais de 1 hora fácil (minhas cópias de ROMs pra emuladores):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20251120141114_screenshot-2025-11-20_13-50-46.png" alt="copiando usb"  loading="lazy" /></p>
<p>O problema é que antes disso o <code>hypridle</code> vai entrar em ação e colocar meu PC pra dormir, pausando essa cópia. E normalmente eu queria deixar copiando, ir dormir, e estar pronto quando acordar. Só que sempre esqueço disso e quando acordo ainda não terminou. Bem inconveniente. Não sei porque o Nautilus já não sinaliza pra inibir sleep quando tem atividades assim acontecendo.</p>
<p>Pode ser que tenha alguma configuração de <code>gsettings</code> que eu não saiba? (se tiver algo assim, não deixem de mandar nos comentários).</p>
<h3>A Solução<span class="hx:absolute hx:-mt-20" id="a-solução"></span>
    <a href="#a-solu%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Enquanto não descubro algo melhor, só tem uma solução: ficar fazendo polling de <code>iostat</code> pra manualmente inibir.</p>
<p>Pra começar precisamos instalar estes dois pacotes no Arch:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -S sysstat bc</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora, criar um script que cheque, vamos chamar de <code>~/.local/bin/io_inhibit.sh</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --- Configuration ---</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UTIL_THRESHOLD: Percentage of time the device is busy (%util) that must be exceeded to inhibit sleep.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 10.0% is a safe low threshold to catch any continuous I/O.</span>
</span></span><span class="line"><span class="cl"><span class="nv">UTIL_THRESHOLD</span><span class="o">=</span>10.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CHECK_INTERVAL: How often (in seconds) to check I/O activity.</span>
</span></span><span class="line"><span class="cl"><span class="nv">CHECK_INTERVAL</span><span class="o">=</span><span class="m">5</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># File to track the PID of the systemd-inhibit process.</span>
</span></span><span class="line"><span class="cl"><span class="nv">PID_FILE</span><span class="o">=</span><span class="s2">&#34;/tmp/io_inhibit_pid&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --- Functions ---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Function to check if the inhibit process is currently running</span>
</span></span><span class="line"><span class="cl">is_inhibitor_running<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$PID_FILE</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">kill</span> -0 <span class="k">$(</span>cat <span class="s2">&#34;</span><span class="nv">$PID_FILE</span><span class="s2">&#34;</span><span class="k">)</span> 2&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="m">0</span> <span class="c1"># True</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="m">1</span> <span class="c1"># False</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Function to get the maximum I/O utilization (%util) across all monitored devices</span>
</span></span><span class="line"><span class="cl">get_max_io_util<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We use -x (extended stats) and monitor all devices.</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The &#39;awk&#39; script finds the column index for &#39;%util&#39; dynamically.</span>
</span></span><span class="line"><span class="cl">    iostat -x <span class="m">1</span> <span class="m">2</span> <span class="p">|</span> awk <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">        BEGIN { max_util = 0.0; f = 0; util_col = 0; }
</span></span></span><span class="line"><span class="cl"><span class="s1">        
</span></span></span><span class="line"><span class="cl"><span class="s1">        # 1. Look for the header line (contains &#39;</span>r/s<span class="s1">&#39; and &#39;</span>util<span class="s1">&#39;). Set flag (f) to start processing.
</span></span></span><span class="line"><span class="cl"><span class="s1">        #    We only do this check in the second block.
</span></span></span><span class="line"><span class="cl"><span class="s1">        /avg-cpu:/ { if (f == 0) f = 1; else if (f == 1) f = 2; next }
</span></span></span><span class="line"><span class="cl"><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">        # 2. In the second block (f == 2), check for the header line to map columns
</span></span></span><span class="line"><span class="cl"><span class="s1">        (f == 2 &amp;&amp; $1 == &#34;Device&#34;) {
</span></span></span><span class="line"><span class="cl"><span class="s1">            # Find the column index for the &#34;%util&#34; header
</span></span></span><span class="line"><span class="cl"><span class="s1">            for (i=1; i&lt;=NF; i++) {
</span></span></span><span class="line"><span class="cl"><span class="s1">                if ($i == &#34;%util&#34;) {
</span></span></span><span class="line"><span class="cl"><span class="s1">                    util_col = i;
</span></span></span><span class="line"><span class="cl"><span class="s1">                    break;
</span></span></span><span class="line"><span class="cl"><span class="s1">                }
</span></span></span><span class="line"><span class="cl"><span class="s1">            }
</span></span></span><span class="line"><span class="cl"><span class="s1">            # Skip the header line after mapping
</span></span></span><span class="line"><span class="cl"><span class="s1">            next
</span></span></span><span class="line"><span class="cl"><span class="s1">        }
</span></span></span><span class="line"><span class="cl"><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">        # 3. Process data lines only after the column has been mapped
</span></span></span><span class="line"><span class="cl"><span class="s1">        (f == 2 &amp;&amp; util_col &gt; 0) { 
</span></span></span><span class="line"><span class="cl"><span class="s1">            # Match lines starting with a common device prefix
</span></span></span><span class="line"><span class="cl"><span class="s1">            if ($1 ~ /^(sd|nvme|loop|hd|dm|zram|mmc)/) {
</span></span></span><span class="line"><span class="cl"><span class="s1">                # Use the dynamically found column index
</span></span></span><span class="line"><span class="cl"><span class="s1">                util = $(util_col);
</span></span></span><span class="line"><span class="cl"><span class="s1">                if (util &gt; max_util) {
</span></span></span><span class="line"><span class="cl"><span class="s1">                    max_util = util;
</span></span></span><span class="line"><span class="cl"><span class="s1">                }
</span></span></span><span class="line"><span class="cl"><span class="s1">            }
</span></span></span><span class="line"><span class="cl"><span class="s1">        }
</span></span></span><span class="line"><span class="cl"><span class="s1">        END { print max_util; }
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --- Main Loop ---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 1. Get current I/O activity</span>
</span></span><span class="line"><span class="cl">    <span class="nv">MAX_UTIL</span><span class="o">=</span><span class="k">$(</span>get_max_io_util<span class="k">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Use &#39;bc&#39; for floating-point comparison</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">((</span> <span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$MAX_UTIL</span><span class="s2"> &gt; </span><span class="nv">$UTIL_THRESHOLD</span><span class="s2">&#34;</span> <span class="p">|</span> bc -l<span class="k">)</span> <span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># I/O is active and exceeds the %util threshold</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> ! is_inhibitor_running<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            setsid systemd-inhibit --what<span class="o">=</span>idle:sleep --why<span class="o">=</span><span class="s1">&#39;Active I/O Detected (%util)&#39;</span> --mode<span class="o">=</span>block sleep infinity <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">            <span class="nb">echo</span> <span class="nv">$!</span> &gt; <span class="s2">&#34;</span><span class="nv">$PID_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># I/O is idle or below threshold</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> is_inhibitor_running<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            <span class="nb">kill</span> <span class="k">$(</span>cat <span class="s2">&#34;</span><span class="nv">$PID_FILE</span><span class="s2">&#34;</span><span class="k">)</span> 2&gt;/dev/null
</span></span><span class="line"><span class="cl">            rm <span class="s2">&#34;</span><span class="nv">$PID_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Wait before checking again</span>
</span></span><span class="line"><span class="cl">    sleep <span class="nv">$CHECK_INTERVAL</span> 
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(sim, este script foi vibe coded e precisei ajustar algumas vezes até funcionar, as versões que o Gemini me dava não funcionavam kkkk).</p>
<p>O principal é o <code>awk</code> processando a saída do comando <code>iostat -x 1 2</code>. Meu micro SD card aparece como <code>/dev/sda</code>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20251120141644_screenshot-2025-11-20_14-16-36.png" alt="iostat"  loading="lazy" /></p>
<p>Note que no segundo bloco, quando estou copiando vários arquivos, ele mostra 99.61 e fica variando entre 90 a 100% de uso, que é o desejável: significa que está usando toda a banda disponível de IO pra ele.</p>
<p>Então, queremos que, enquanto estiver numa taxa acima de zero, tipo, maior que 10% de atividade, o HyprIdle não ative o sleep no timeout normal.</p>
<p>Com esse script, agora temos o seguinte quando listamos <code>systemd-inhibit --list</code>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20251120141859_screenshot-2025-11-20_14-18-48.png" alt="sleep infinity"  loading="lazy" /></p>
<p>Agora apareceu uma nova linha dizendo:</p>
<ul>
<li>
<p><code>sleep infinity</code>: Este é o nome do processo que iniciou meu script e segura o lock.</p>
</li>
<li>
<p><code>sleep:idle</code>: Esta é a parte crucial. Diz ao systemd e logind pra bloquear tanto sleep (system suspend) quanto idle (prevenindo timers de idle como do hypridle).</p>
</li>
<li>
<p><code>Active I/O Detected (%util)</code>: Esta é a razão especificada no script.</p>
</li>
</ul>
<p>E pronto, pra garantir que temos este script sempre rodando, basta alterar o <code>~/.config/hypr/autostart.conf</code> pra ter esta linha:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>exec-once = /home/akitaonrails/.local/bin/io_inhibit.sh &amp;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Em teoria, agora posso deixar essas atividades longas de IO rodando e não me preocupar do meu Arch suspender e dormir.</p>
<p>Alguém conhece alguma solução mais simples do que essa? Se tiver idéias, mandem nos comentários.</p>
]]></content:encoded><category>omarchy</category><category>linux</category><category>automacao</category></item><item><title>Meu Novo Blog - Como Eu Fiz</title><link>https://akitaonrails.github.io/2025/09/10/meu-novo-blog-como-eu-fiz/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/09/10/meu-novo-blog-como-eu-fiz/</guid><pubDate>Wed, 10 Sep 2025 15:00:00 -0300</pubDate><description>&lt;p&gt;Este artigo está alguns meses atrasado, mas resolvi documentar agora porque senão vou esquecer como fiz. Como breve introdução, já digo o maior erro que todo programador que quer tentar começar a escrever comete: perde MUITO tempo tentando criar o blog perfeito.
&lt;strong&gt;Just f&amp;hellip;ng write!!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910134801_screenshot-2025-09-10_13-47-43.png" alt="novo blog" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;(e sim, tem tema dark, link no rodapé do site)&lt;/p&gt;
&lt;h3&gt;O BLOG PERFEITO NÃO EXISTE&lt;span class="hx:absolute hx:-mt-20" id="o-blog-perfeito-não-existe"&gt;&lt;/span&gt;
&lt;a href="#o-blog-perfeito-n%c3%a3o-existe" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Eu acho importante que todo programador tente escrever. Não pra virar influencer nem nada disso. O primeiro objetivo é &lt;strong&gt;escrever pra você mesmo&lt;/strong&gt;. Eu uso meu blog primariamente como um &amp;ldquo;backup&amp;rdquo; do meu cérebro (assim como foi meu canal do YouTube).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este artigo está alguns meses atrasado, mas resolvi documentar agora porque senão vou esquecer como fiz. Como breve introdução, já digo o maior erro que todo programador que quer tentar começar a escrever comete: perde MUITO tempo tentando criar o blog perfeito.
<strong>Just f&hellip;ng write!!</strong></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910134801_screenshot-2025-09-10_13-47-43.png" alt="novo blog"  loading="lazy" /></p>
<p>(e sim, tem tema dark, link no rodapé do site)</p>
<h3>O BLOG PERFEITO NÃO EXISTE<span class="hx:absolute hx:-mt-20" id="o-blog-perfeito-não-existe"></span>
    <a href="#o-blog-perfeito-n%c3%a3o-existe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu acho importante que todo programador tente escrever. Não pra virar influencer nem nada disso. O primeiro objetivo é <strong>escrever pra você mesmo</strong>. Eu uso meu blog primariamente como um &ldquo;backup&rdquo; do meu cérebro (assim como foi meu canal do YouTube).</p>
<p>Notem como os últimos posts de Linux não sou eu tentando te vender nada, mas sim coisas que fiz de verdade no meu PC ou home server, e anoto passo a passo. Caso daqui 3 anos eu queira refazer, tenho tudo anotado.</p>
<p>Segundo: programadores são <strong>péssimos</strong> comunicadores. Nós falamos somente pra nós mesmos e não pensamos se a pessoa ouvindo está nos entendendo. E quando vemos que não entende a reação é <em>&ldquo;ah, porque ela é burra e eu sou inteligente.&rdquo;</em></p>
<p>Falei sobre isso no artigo <a href="https://akitaonrails.com/2013/11/02/off-topic-programadores-sao-pessimos-comunicadores-udp-vs-tcp/"target="_blank" rel="noopener">Programadores são Péssimos Comunicadores</a>. Mesmo se só uma pessoa ler seu post, mas te der um comentário, esse feedback é importante. É como um teste rodando que falha: agora você sabe onde tem que corrigir. E vai corrigindo, um passo de cada vez.</p>
<p>Portanto, escrever blog post, não importa se tem zero visitantes, é um exercício de prática pra se tornar um comunicador melhor, por isso recomendo.</p>
<h3>Blog Engines<span class="hx:absolute hx:-mt-20" id="blog-engines"></span>
    <a href="#blog-engines" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu comecei postando no Google Blogspot em 2006. Depois fiz meu próprio blog num projeto que já existia em Ruby on Rails 2.0, passei por vários, como o Typo3 na época. Até que em 2012 eu fiz o meu próprio engine do zero usando ActiveAdmin e é esse projeto que eu vim atualizando de Rails 3 até Rails 6 ou 7 recentemente. Meu primeiro post continua no blog:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910144909_screenshot-2025-09-10_14-48-55.png" alt="primeiro post"  loading="lazy" /></p>
<p>Muita coisa mudou nesse período, Less foi substituído por Sass. Textile e Liquid foram substituídos por Markdown, surgiu Twitter Bootstrap, etc. Por isso dezenas de artigos que escrevi estavam em Textile. E como sempre tive preguiça de converter em Markdown, demorei pra sair dessa minha engine velha.</p>
<p>Pra um blog pessoal, não precisa de nada complicado, como banco de dados ou coisas assim, mas acabou vindo como legado da época. Eu já conhecia <a href="https://github.com/jekyll/jekyll"target="_blank" rel="noopener">jekyll</a> pra fazer blogs com páginas estáticas, mas ainda tinha os posts velhos em Textile que tava com preguiça de lidar.</p>
<p>Ao longo do tempo também vim apagando vários posts. Coisas que ficaram obsoletas mesmo como &ldquo;dicas pra Ruby 2.0&rdquo; ou &ldquo;novidades no Rails 3.0&rdquo;. Ler isso hoje não faria sentido. Fui mantendo somente os posts que se ler hoje, ainda faz sentido. Isso veio a me ajudar quando resolvi converter os posts velhos.</p>
<p>Outro motivo de continuar no meu engine era que meu ActiveAdmin tinha o suporte pra fazer upload de imagens no meu bucket da AWS S3, então era uma conveniência extra que não tava com vontade de ter que resolver de outra forma.</p>
<p>Finalmente, em 2025, resolvi que estava de saco cheio de ficar atualizando versões de Rails, fazendo deploy no Heroku, lidar com backup de banco de dados, além do front-end estar feio e quebrado.</p>
<h3>Hextra/Hugo<span class="hx:absolute hx:-mt-20" id="hextrahugo"></span>
    <a href="#hextrahugo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Depois de Jekyll, surgiram geradores de sites estáticos em dezenas de linguagens. De fato, é o jeito mais prático pra um programador escrever um blog: só criar um arquivo texto localmente, escrever tudo em markdown, rodar um script e subir um site estático que não precisa de nenhuma configuração, banco de dados ou qualquer outro componente mais complicado. Dá pra subir até como GitHub Pages e hospedar lá grátis.</p>
<p>Por isso eu sabia que, se fosse fazer um novo engine, teria que ser site estático. Já tinha ouvido falar do tal projeto <a href="https://github.com/gohugoio/hugo"target="_blank" rel="noopener"><strong>Hugo</strong></a>, que é feito em Go e tem tudo que eu precisava: markdown, suporte a tags, etc. Mas tinha um problema: eu odeio fazer front-end e eu nunca fui um bom web designer, ou designer no geral.</p>
<p>Foi quando esbarrei no <a href="https://themes.gohugo.io/themes/hextra/"target="_blank" rel="noopener"><strong>Hextra</strong></a>, que é basicamente o Hugo mas com vários temas pré-prontos. E diferente de vários outros projetos com temas gratuitos que costumam ser muito feios e muito mal feitos, os do Hextra me pareceram minimamente bem feitos. Fucei um pouco, achei um tema padrão minimalista que é legível pra textos com código e decidi que seria agora.</p>
<p>Desta vez não vou dar passo a passo de tudo que eu fiz porque seria maçante, mas é só ler a <a href="https://gohugo.io/commands/hugo/"target="_blank" rel="noopener">documentação do Hugo</a> pra saber o que fazer. Na prática:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># novo projeto</span>
</span></span><span class="line"><span class="cl">hugo new
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># reconstruir o site</span>
</span></span><span class="line"><span class="cl">hugo build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># subir o servidor local (porta 1313)</span>
</span></span><span class="line"><span class="cl">hugo server --buildDrafts --disableFastRender</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No meu caso, eu comecei com o <a href="https://github.com/imfing/hextra-starter-template"target="_blank" rel="noopener"><strong>Hextra Starter Template</strong></a> do que começar do Hugo zerado. Acho que vale a pena checar. Ele também explica como fazer deploy pra GitHub Pages e Netlify.</p>
<h3>Código Aberto<span class="hx:absolute hx:-mt-20" id="código-aberto"></span>
    <a href="#c%c3%b3digo-aberto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A vantagem de usar um gerador de site estático é que posso me livrar do banco de dados Postgres que usei por anos e converter todos os artigos em arquivos texto individuais. Feito isso, basta subir tudo num repositório no GitHub. E como são textos publicamente disponíveis de qualquer forma, não tem porque não deixar aberto. Dessa forma, qualquer um pode contribuir (e muitos já contribuíram - obrigado pela ajuda!)</p>
<p>Se quiser contribuir ou só fuçar acesse o <a href="https://github.com/akitaonrails/akitaonrails.github.io"target="_blank" rel="noopener">repositório no GitHub</a> e faça um fork. Note como o nome do projeto é <code>akitaonrails.github.io</code> porque, originalmente, eu pretendia fazer deploy no GitHub Pages, mas mudei de idéia e já falo disso.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910140229_screenshot-2025-09-10_14-02-12.png" alt="contribuições"  loading="lazy" /></p>
<p>Até agora já tive 8 Pull Requests mergeadas. Alguns realmente se deram ao trabalho de caçar erros, typos, bugs e mandaram correções muito boas. O blog está melhor graças a eles. Às vezes eu posso demorar um pouco pra checar os PRs, mas eventualmente eu chego e costumo mergear tudo.</p>
<p>Um dos PRs inclusive foi pra adicionar suporte a Docker Compose pra facilitar pra quem quiser contribuir. Outro PR foi pra melhorar a documentação na página de README. Então tá bem fácil de entender o que fazer.</p>
<p>Outra contribuição importante foi adicionar tag pra fazer embed de video do YouTube. Eu só preciso fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/HCHqtpipwu4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>
</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Do jeito antigo eu ia no YouTube, clicava em &ldquo;Share&rdquo;, &ldquo;Embed&rdquo; e copiava o bloco de iframe. Mas isso quebra em mobile.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910140536_screenshot-2025-09-10_14-05-20.png" alt="readme"  loading="lazy" /></p>
<p>Eu particularmente não uso o workflow de Docker, logo abaixo tem o workflow pra instalação local. São tão poucos componentes que não acho que valha a pena lidar com Docker pra isso.</p>
<p>Digamos que eu queira escrever este artigo. Ele está salvo em <code>content/09/10/meu-novo-blog-como-eu-fiz/index.md</code> que significa mês 09 (setembro), dia 10, seguido do slug como sub-diretório e dentro só um <code>index.md</code> que é markdown. As primeiras linhas do artigo tem este cabeçalho:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Meu Novo Blog - Como Eu Fiz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2025-09-10T15:00:00-03:00&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">slug</span><span class="p">:</span><span class="w"> </span><span class="l">meu-novo-blog-como-eu-fiz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tags</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">blog</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">hextra</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ruby</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">draft</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">...</span><span class="l">.</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E segue o artigo escrito em Markdown. Feito isso, do diretório do projeto tenho que fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> content
</span></span><span class="line"><span class="cl">ruby generate_index.rb
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ..</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso atualiza a página principal com o link pro novo artigo no topo. Pra garantir que está tudo certo, posso checar subindo o servidor local:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">❯ hugo server --buildDrafts --disableFastRender
</span></span><span class="line"><span class="cl">Watching <span class="k">for</span> changes in /home/akitaonrails/Projects/akitaonrails-hugo/<span class="o">{</span>content,i18n,layouts<span class="o">}</span>
</span></span><span class="line"><span class="cl">Watching <span class="k">for</span> config changes in /home/akitaonrails/Projects/akitaonrails-hugo/hugo.yaml, /home/akitaonrails/Projects/akitaonrails-hugo/go.mod
</span></span><span class="line"><span class="cl">Start building sites …
</span></span><span class="line"><span class="cl">hugo v0.149.1+extended+withdeploy linux/amd64 <span class="nv">BuildDate</span><span class="o">=</span>unknown
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                  │  EN
</span></span><span class="line"><span class="cl">──────────────────┼──────
</span></span><span class="line"><span class="cl"> Pages            │ <span class="m">3374</span>
</span></span><span class="line"><span class="cl"> Paginator pages  │    <span class="m">0</span>
</span></span><span class="line"><span class="cl"> Non-page files   │  <span class="m">131</span>
</span></span><span class="line"><span class="cl"> Static files     │   <span class="m">11</span>
</span></span><span class="line"><span class="cl"> Processed images │    <span class="m">0</span>
</span></span><span class="line"><span class="cl"> Aliases          │    <span class="m">0</span>
</span></span><span class="line"><span class="cl"> Cleaned          │    <span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Built in <span class="m">8799</span> ms
</span></span><span class="line"><span class="cl">Environment: <span class="s2">&#34;development&#34;</span>
</span></span><span class="line"><span class="cl">Serving pages from disk
</span></span><span class="line"><span class="cl">Web Server is available at http://localhost:1313/ <span class="o">(</span><span class="nb">bind</span> address 127.0.0.1<span class="o">)</span>
</span></span><span class="line"><span class="cl">Press Ctrl+C to stop</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora posso acessar no navegador em <code>http://localhost:1313</code>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141059_screenshot-2025-09-10_14-10-46.png" alt="hugo server"  loading="lazy" /></p>
<p>Finalmente, basta deployar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add .
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;adding new blog article&#34;</span>
</span></span><span class="line"><span class="cl">git push origin master</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E é só isso!</p>
<h3>Deploy<span class="hx:absolute hx:-mt-20" id="deploy"></span>
    <a href="#deploy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Falei que pretendia fazer deploy no GitHub Pages, mas na última hora resolvi que queria fazer deploy na [Netlify]</p>
<p>Por que? Porque eu nunca tinha usado, então resolvi que queria ver como era. Basicamente isso. Eu achei barato, tem as funcionalidades que precisava, então, por que não?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141436_screenshot-2025-09-10_14-14-25.png" alt="netlify project"  loading="lazy" /></p>
<p>A única coisa que precisei fazer de diferente, foi configurar o deploy linkando com meu repositório do Github:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141645_screenshot-2025-09-10_14-16-25.png" alt="netlify deploy"  loading="lazy" /></p>
<p>E, no GitHub, criar uma Action assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Sample workflow for building and deploying a Hugo site to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy Hugo site to Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Runs on pushes targeting the default branch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;master&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Allows you to run this workflow manually from the Actions tab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">concurrency</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pages&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cancel-in-progress</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Default to bash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">defaults</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">bash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Build job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HUGO_VERSION</span><span class="p">:</span><span class="w"> </span><span class="m">0.145.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Checkout</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">fetch-depth</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">  </span><span class="c"># fetch all history for .GitInfo and .Lastmod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">submodules</span><span class="p">:</span><span class="w"> </span><span class="l">recursive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Go</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-go@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">go-version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;1.24&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/configure-pages@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
</span></span></span><span class="line"><span class="cl"><span class="sd">          &amp;&amp; sudo dpkg -i ${{ runner.temp }}/hugo.deb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build with Hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># For maximum backward compatibility with Hugo modules</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">HUGO_ENVIRONMENT</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">HUGO_ENV</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          hugo \
</span></span></span><span class="line"><span class="cl"><span class="sd">            --gc --minify \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --baseURL &#34;https://${{ github.repository_owner }}.github.io/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Upload artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/upload-pages-artifact@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./public</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Deployment job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">github-pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">${{ steps.deployment.outputs.page_url }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/deploy-pages@v4</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com isso, toda vez que faço <code>git push</code>, vai rodar esse job, gerar o site e mandar pro Netlify direto. Tudo automatizado, como Deus sempre quis 🤣:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141920_screenshot-2025-09-10_14-19-03.png" alt="GitHub Actions"  loading="lazy" /></p>
<p>Aliás, eu falei que o Netlify era &ldquo;barato&rdquo;, na verdade eu quis dizer que é <strong>gratuito</strong>. Pra sites pequenos como o meu, não custa literalmente nada:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910142044_screenshot-2025-09-10_14-20-30.png" alt="netlify plan"  loading="lazy" /></p>
<p>Pago zero dólares e ainda aprendi algo novo. Por isso essas coisas são divertidas.</p>
<h3>Scripts<span class="hx:absolute hx:-mt-20" id="scripts"></span>
    <a href="#scripts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra converter os artigos do meu blog antigo pro Hugo, acabei fazendo alguns scripts de correção. Eles ainda estão no repositório como <code>content/fix_*.rb</code>. Não precisa saber pra que eles servem, mas estão lá caso queiram fuçar. O mais importante era manter as URIs iguais eram no blog anterior pro Google continuar achando corretamente.</p>
<p>O único que realmente interessa é o <code>generate_index.rb</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;yaml&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;find&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;date&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">escape_markdown</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">text</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="s1">&#39;[&#39;</span><span class="p">,</span> <span class="s1">&#39;\\[&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="s1">&#39;]&#39;</span><span class="p">,</span> <span class="s1">&#39;\\]&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">entries</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">Find</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;.&#39;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="k">next</span> <span class="k">unless</span> <span class="n">path</span><span class="o">.</span><span class="n">end_with?</span><span class="p">(</span><span class="s1">&#39;index.md&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">next</span> <span class="k">if</span> <span class="n">path</span> <span class="o">==</span> <span class="s1">&#39;./index.md&#39;</span> <span class="o">||</span> <span class="n">path</span> <span class="o">==</span> <span class="s1">&#39;./_index.md&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">begin</span>
</span></span><span class="line"><span class="cl">    <span class="n">lines</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">readlines</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">lines</span><span class="o">.</span><span class="n">first</span><span class="o">&amp;.</span><span class="n">strip</span> <span class="o">==</span> <span class="s1">&#39;---&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="n">fm_lines</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">      <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">      <span class="k">while</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">lines</span><span class="o">.</span><span class="n">size</span> <span class="o">&amp;&amp;</span> <span class="n">lines</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="n">strip</span> <span class="o">!=</span> <span class="s1">&#39;---&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="n">fm_lines</span> <span class="o">&lt;&lt;</span> <span class="n">lines</span><span class="o">[</span><span class="n">i</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">lines</span><span class="o">[</span><span class="n">i</span><span class="o">]&amp;.</span><span class="n">strip</span> <span class="o">==</span> <span class="s1">&#39;---&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="n">front</span> <span class="o">=</span> <span class="no">YAML</span><span class="o">.</span><span class="n">safe_load</span><span class="p">(</span><span class="n">fm_lines</span><span class="o">.</span><span class="n">join</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">front</span> <span class="o">&amp;&amp;</span> <span class="n">front</span><span class="o">[</span><span class="s1">&#39;title&#39;</span><span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="n">front</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">          <span class="n">date</span> <span class="o">=</span> <span class="no">Date</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">front</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">].</span><span class="n">to_s</span><span class="p">)</span> <span class="k">rescue</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl">          <span class="k">if</span> <span class="n">date</span>
</span></span><span class="line"><span class="cl">            <span class="n">url</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="s1">&#39;./&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="s1">&#39;/index.md&#39;</span><span class="p">,</span> <span class="s1">&#39;/&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">entries</span> <span class="o">&lt;&lt;</span> <span class="p">{</span> <span class="s1">&#39;title&#39;</span> <span class="o">=&gt;</span> <span class="n">front</span><span class="o">[</span><span class="s1">&#39;title&#39;</span><span class="o">]</span><span class="p">,</span> <span class="s1">&#39;url&#39;</span> <span class="o">=&gt;</span> <span class="n">url</span><span class="p">,</span> <span class="s1">&#39;date&#39;</span> <span class="o">=&gt;</span> <span class="n">date</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="k">end</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="nb">warn</span> <span class="s2">&#34;YAML error in </span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sort newest first</span>
</span></span><span class="line"><span class="cl"><span class="n">entries</span><span class="o">.</span><span class="n">sort_by!</span> <span class="p">{</span> <span class="o">|</span><span class="n">e</span><span class="o">|</span> <span class="n">e</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">]</span> <span class="p">}</span><span class="o">.</span><span class="n">reverse!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Group by year and month</span>
</span></span><span class="line"><span class="cl"><span class="n">grouped</span> <span class="o">=</span> <span class="n">entries</span><span class="o">.</span><span class="n">group_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">e</span><span class="o">|</span> <span class="o">[</span><span class="n">e</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">].</span><span class="n">year</span><span class="p">,</span> <span class="n">e</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">].</span><span class="n">month</span><span class="o">]</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sort year-month pairs descending</span>
</span></span><span class="line"><span class="cl"><span class="n">sorted_keys</span> <span class="o">=</span> <span class="n">grouped</span><span class="o">.</span><span class="n">keys</span><span class="o">.</span><span class="n">sort</span><span class="o">.</span><span class="n">reverse</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">File</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s1">&#39;_index.md&#39;</span><span class="p">,</span> <span class="s1">&#39;w&#39;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;---&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;title: AkitaOnRails&#39;s Blog&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;---&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">sorted_keys</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="p">(</span><span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="p">)</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">month_name</span> <span class="o">=</span> <span class="no">Date</span><span class="o">::</span><span class="no">MONTHNAMES</span><span class="o">[</span><span class="n">month</span><span class="o">]</span> <span class="c1"># &#34;May&#34;, &#34;June&#34;, etc</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;## </span><span class="si">#{</span><span class="n">year</span><span class="si">}</span><span class="s2"> - </span><span class="si">#{</span><span class="n">month_name</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">grouped</span><span class="o">[[</span><span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="o">]].</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">post</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;- [</span><span class="si">#{</span><span class="n">escape_markdown</span><span class="p">(</span><span class="n">post</span><span class="o">[</span><span class="s1">&#39;title&#39;</span><span class="o">]</span><span class="p">)</span><span class="si">}</span><span class="s2">](</span><span class="si">#{</span><span class="n">post</span><span class="o">[</span><span class="s1">&#39;url&#39;</span><span class="o">]</span><span class="si">}</span><span class="s2">)&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">puts</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">puts</span> <span class="s2">&#34;Generated _index.md with posts grouped by year &amp; month.&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse script tem um pequeno bug: ele ordena os artigos a partir do diretório de mês e dia, mas significa que se eu postar várias vezes durante o dia, vai sair fora de ordem, ordenado pelo título do artigo. Veja: <code>sorted_keys.each do |(year, month)|</code></p>
<p>O certo seria abrir o artigo e pegar o metadado de timestamp, mas fiquei com preguiça. Se alguém quiser corrigir e mandar um PR, será bem vindo.</p>
<p>Por que fiz em Ruby e não em Python ou Bash?? Porque foi como eu quis 🤣, não perturba.</p>
<h3>Imagens no S3<span class="hx:absolute hx:-mt-20" id="imagens-no-s3"></span>
    <a href="#imagens-no-s3" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como contei antes, uma das conveniências do meu antigo engine feito em Rails, é que eu tinha feito o ActiveAdmin com suporte a fazer uploads pro meu bucket de S3. E era um setup complicado porque ainda adicionava AWS CloudFront na frente pra fazer cache dessas imagens em CDN e tudo mais.</p>
<p>Mas meu site é ultra-pequeno, não precisa da complexidade de CloudFront, então tirei e agora faço links direto pro bucket, por exemplo: <code>https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/screenshot-2025-09-10_00-35-57.png</code></p>
<p>Significa que meu workflow consiste em tirar screenshots, depois abrir o site de console do meu AWS, e manualmente fazer upload no bucket, via interface web, copiar o link gerado e colar no Markdown do artigo. Não é complicado, mas é um BEM chatinho.</p>
<p>Cheguei a fazer um script besta usando <a href="https://github.com/aws/aws-cli"target="_blank" rel="noopener"><strong>aws-cli</strong></a> mas isso exigia manter um terminal aberto no diretório de screenshots e, manualmente fazer algo como <code>upload-s3 screenshot-2025-09-10_00-35-57.png</code>, o que não é exatamente conveniente também. Aliás, se não sabia disso, você pode automatizar muita coisa da AWS só usando essa ferramenta.</p>
<p>Enquanto estava pensando em escrever este artigo, resolvi consertar esse problema de uma vez. Como sabem, estou usando Omarchy, e o File Explorer padrão dele é o bom e velho <strong>Nautilus</strong> do GNOME. Gosto dele, parece muito com o Finder de MacOS. Gosto mais dele do que um Dolphin ou Thunar, que parecem mais com o Windows Explorer. E eu nunca gostei de Windows Explorer.</p>
<blockquote>
  <p><strong>&ldquo;E se eu pudesse só clicar com o botão direito do mouse em cima do arquivo de screenshot e selecionar &rsquo;enviar pro s3&rsquo; no menu de contexto e já ganhar a URL copiada no meu clipboard? 🤔&rdquo;</strong></p>

</blockquote>
<p>Que tal assim?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910143157_send-to-s3.png" alt="nautilus send to s3"  loading="lazy" /></p>
<p>Nautilus suporta scripts de usuário, então fiz o seguinte:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.local/share/nautilus/scripts
</span></span><span class="line"><span class="cl">nvim ~/.local/share/nautilus/scripts/Send<span class="se">\ </span>to<span class="se">\ </span>AWS<span class="se">\ </span>S3</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E eis o script:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="c1">#set -euo pipefail</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ---------- config ----------</span>
</span></span><span class="line"><span class="cl"><span class="nv">BUCKET</span><span class="o">=</span><span class="s2">&#34;new-uploads-akitaonrails&#34;</span> <span class="c1"># name only</span>
</span></span><span class="line"><span class="cl"><span class="nv">PREFIX</span><span class="o">=</span><span class="s2">&#34;&#34;</span>                         <span class="c1"># e.g., &#34;uploads/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">AWS_PROFILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">AWS_PROFILE</span><span class="k">:-</span><span class="nv">default</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># URL_MODE: presign | public | cloudfront</span>
</span></span><span class="line"><span class="cl"><span class="nv">URL_MODE</span><span class="o">=</span><span class="s2">&#34;public&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">PRESIGN_TTL</span><span class="o">=</span><span class="m">604800</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLOUDFRONT_DOMAIN</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">MAKE_PUBLIC_ACL</span><span class="o">=</span><span class="nb">false</span> <span class="c1"># keep false unless your bucket denies public reads</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ----------------------------</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">require<span class="o">()</span> <span class="o">{</span> <span class="nb">command</span> -v <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># clipboard: wl-copy &gt; xclip &gt; none</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLIP_CMD</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> require wl-copy<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">CLIP_CMD</span><span class="o">=</span><span class="s2">&#34;wl-copy&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> require xclip<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">CLIP_CMD</span><span class="o">=</span><span class="s2">&#34;xclip -selection clipboard&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">region<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> r
</span></span><span class="line"><span class="cl">  <span class="nv">r</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>aws --profile <span class="s2">&#34;</span><span class="nv">$AWS_PROFILE</span><span class="s2">&#34;</span> s3api get-bucket-location --bucket <span class="s2">&#34;</span><span class="nv">$BUCKET</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --query <span class="s1">&#39;LocationConstraint&#39;</span> --output text 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;None&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;None&#34;</span> <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;null&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;us-east-1&#34;</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">public_url<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="nv">r</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$CLOUDFRONT_DOMAIN</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;cloudfront&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">printf</span> <span class="s1">&#39;https://%s/%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$CLOUDFRONT_DOMAIN</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;us-east-1&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">printf</span> <span class="s1">&#39;https://%s.s3.amazonaws.com/%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$BUCKET</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="nb">printf</span> <span class="s1">&#39;https://%s.s3.%s.amazonaws.com/%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$BUCKET</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">presign_url<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  aws --profile <span class="s2">&#34;</span><span class="nv">$AWS_PROFILE</span><span class="s2">&#34;</span> s3 presign <span class="s2">&#34;s3://</span><span class="nv">$BUCKET</span><span class="s2">/</span><span class="nv">$key</span><span class="s2">&#34;</span> --expires-in <span class="s2">&#34;</span><span class="nv">$PRESIGN_TTL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">upload_one<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">src</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span> <span class="nv">args</span><span class="o">=(</span>--only-show-errors --follow-symlinks<span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;public&#34;</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$MAKE_PUBLIC_ACL</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;true&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">args</span><span class="o">+=(</span>--acl public-read<span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  aws --profile <span class="s2">&#34;</span><span class="nv">$AWS_PROFILE</span><span class="s2">&#34;</span> s3 cp <span class="s2">&#34;</span><span class="si">${</span><span class="nv">args</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$src</span><span class="s2">&#34;</span> <span class="s2">&#34;s3://</span><span class="nv">$BUCKET</span><span class="s2">/</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># allow running from terminal with paths as args</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="nv">$#</span> -gt <span class="m">0</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">NAUTILUS_SCRIPT_SELECTED_FILE_PATHS</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SEL</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">NAUTILUS_SCRIPT_SELECTED_FILE_PATHS</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$SEL</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">||</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  notify-send <span class="s2">&#34;S3 upload&#34;</span> <span class="s2">&#34;No selection&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">R</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>region<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">urls</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="nv">ok</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="nv">fail</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="nv">errs</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r path<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> -e <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">||</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">errs</span><span class="o">+=(</span><span class="s2">&#34;Missing: </span><span class="nv">$path</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">((</span>fail++<span class="o">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">base</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r -d <span class="s1">&#39;&#39;</span> f<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="nv">rel</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">f</span><span class="p">#</span><span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span><span class="p">/</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">PREFIX</span><span class="si">}${</span><span class="nv">base</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">rel</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> upload_one <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">        presign<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>presign_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">        public <span class="p">|</span> cloudfront<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>public_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$R</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">        <span class="k">esac</span>
</span></span><span class="line"><span class="cl">        <span class="o">((</span>ok++<span class="o">))</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="nv">errs</span><span class="o">+=(</span><span class="s2">&#34;Failed: </span><span class="nv">$f</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">((</span>fail++<span class="o">))</span>
</span></span><span class="line"><span class="cl">      <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">done</span> &lt; &lt;<span class="o">(</span>find <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> -type f -print0<span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nv">ts</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date +%Y%m%d%H%M%S<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">filename</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">PREFIX</span><span class="si">}${</span><span class="nv">ts</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">filename</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> upload_one <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">      presign<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>presign_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      public <span class="p">|</span> cloudfront<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>public_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$R</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      <span class="k">esac</span>
</span></span><span class="line"><span class="cl">      <span class="o">((</span>ok++<span class="o">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="nv">errs</span><span class="o">+=(</span><span class="s2">&#34;Failed: </span><span class="nv">$path</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">((</span>fail++<span class="o">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$SEL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">out</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">urls</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --- relax only for UX bits ---</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> +e
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;%s&#39;</span> <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="p">|</span> wl-copy -n <span class="o">||</span> <span class="nb">true</span> <span class="c1"># or xclip fallback</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">command</span> -v notify-send &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">((</span><span class="nv">fail</span> <span class="o">==</span> 0<span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    notify-send <span class="s2">&#34;S3 upload: </span><span class="si">${</span><span class="nv">ok</span><span class="si">}</span><span class="s2"> ok&#34;</span> <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">urls</span><span class="p">[@]:</span><span class="nv">0</span><span class="p">:</span><span class="nv">5</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    notify-send -u critical <span class="s2">&#34;S3 upload: </span><span class="si">${</span><span class="nv">ok</span><span class="si">}</span><span class="s2"> ok, </span><span class="si">${</span><span class="nv">fail</span><span class="si">}</span><span class="s2"> failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">errs</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> head -n 10<span class="k">)</span><span class="s2">&#34;</span> <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -e
</span></span><span class="line"><span class="cl"><span class="c1"># --- end relax ---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">((</span><span class="nv">fail</span> <span class="o">==</span> 0<span class="o">))</span> <span class="o">||</span> <span class="nb">exit</span> <span class="m">1</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Obviamente troque o <code>BUCKET</code> pelo seu bucket. Se tiver múltiplos buckets, depois reorganize esse script e faça <code>source</code> em múltiplos scripts como <code>Send To Bucket XYZ</code> e <code>Send to Bucket ABC</code> ou algo assim no diretório de <code>nautilus/scripts</code>.</p>
<p>Se quiser testar do terminal, só fazer assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># single file</span>
</span></span><span class="line"><span class="cl"><span class="nv">NAUTILUS_SCRIPT_SELECTED_FILE_PATHS</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s1">&#39;/full/path/to/image.png&#39;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">bash -x ~/.local/share/nautilus/scripts/Send<span class="se">\ </span>to<span class="se">\ </span>AWS<span class="se">\ </span>S3</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notem que comentei o <code>set -e</code> do Bash porque ou o <code>wl-copy</code> ou o <code>notify-send</code> está retornando algum warning ou erro e isso está quebrando o script. Sem isso, termina corretamente e copia a URL do upload no clipboard. Daí posso colar direto no editor de textos.</p>
<h3>Bônus: Screenshots em Omarchy<span class="hx:absolute hx:-mt-20" id="bônus-screenshots-em-omarchy"></span>
    <a href="#b%c3%b4nus-screenshots-em-omarchy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><blockquote>
  <p>Inclusive, dica bônus de Omarchy: do nada o script de screenshot começou a falhar pra mim. Vamos consertar.</p>

</blockquote>
<p>Primeiro, eu queria que os screenshots fossem pro diretório <code>Pictures/Screenshots</code>, segundo, depois de uma atualização, sei lá porque o comando <code>hyprshot</code> parou de funcionar com o <code>satty</code>. Sem paciência durante a madrugada eu editei o script de screenshot do Omarchy, que fica em <code>~/.local/share/omarchy/bin/omarchy-cmd-screenshot</code> (fucem lá), e substituí por isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">[[</span> -f ~/.config/user-dirs.dirs <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> ~/.config/user-dirs.dirs
</span></span><span class="line"><span class="cl"><span class="nv">OUT</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">OMARCHY_SCREENSHOT_DIR</span><span class="k">:-</span><span class="si">${</span><span class="nv">XDG_PICTURES_DIR</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/Pictures</span><span class="si">}}</span><span class="s2">/Screenshots&#34;</span>
</span></span><span class="line"><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$OUT</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">satty --filename - <span class="se">\
</span></span></span><span class="line"><span class="cl">  --output-filename <span class="s2">&#34;</span><span class="nv">$OUT</span><span class="s2">/screenshot-</span><span class="k">$(</span>date +<span class="s1">&#39;%Y-%m-%d_%H-%M-%S&#39;</span><span class="k">)</span><span class="s2">.png&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --early-exit <span class="se">\
</span></span></span><span class="line"><span class="cl">  --actions-on-enter save-to-clipboard <span class="se">\
</span></span></span><span class="line"><span class="cl">  --save-after-copy <span class="se">\
</span></span></span><span class="line"><span class="cl">  --copy-command <span class="s1">&#39;wl-copy&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  &lt; &lt;<span class="o">(</span>slurp <span class="p">|</span> grim -g - -<span class="o">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Na prática, troquei o <code>hyprshot</code> pelo <a href="https://github.com/emersion/grim"target="_blank" rel="noopener"><strong>grim</strong></a>, que permite capturar foto de tela de Wayland (cuidado que tem várias ferramentas que só funcionam em X11, Omarchy é Hyprland, portanto, Wayland).</p>
<p>Além disso o Omarchy não tem opção pra tirar foto de tela com <strong>timer</strong>. Isso é útil quando quero tirar foto de um menu aberto com item selecionado. Se usar o jeito normal com tecla Print-Screen ou usando o menu do Omarchy, o menu que eu tinha aberto pode fechar antes de conseguir a foto de tela. A solução é um timer.</p>
<p>E com <code>grim</code> é fácil, via linha de comando mesmo posso fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sleep <span class="m">10</span> <span class="o">&amp;&amp;</span> grim -g <span class="s2">&#34;2813,948 2560x1440&#34;</span> ~/Pictures/Screenshots/image-01.png</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Em Wayland, dois monitores são como se fossem uma única tela gigante, então precisa dar a geometria correta de onde quero tirar a foto. ChatGPT ajuda pra isso se passar o seu <code>.config/hypr/monitors.conf</code> pra ele.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910150146_screenshot-2025-09-10_15-01-32.png" alt="chatgpt"  loading="lazy" /></p>
<p>Agora tenho uma forma simples de tirar fotos de tela e enviar direto pro S3, direto do Nautilus, e conseguindo tirar uma foto de tela como esta:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910144429_nautilus-script.png" alt="Pinta"  loading="lazy" /></p>
<p>E o programa que eu uso, pra fazer coisas simples como um <strong>crop</strong> de alguma foto de tela, é o <a href="https://github.com/PintaProject/Pinta"target="_blank" rel="noopener"><strong>Pinta</strong></a></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910144520_screenshot-2025-09-10_14-45-05.png" alt="Pinta"  loading="lazy" /></p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso é o que lembrei de importante sobre meu novo blog. Acho que ficou muito mais simples de dar manutenção e, mais importante, muito mais simples de escrever artigos: só abrir um editor de textos e pronto. Nenhum deploy mais complicado, tudo automatizado, tudo integrado com meu Omarchy.</p>
<p>Meu blog já tem mais de <strong>20 anos</strong> e durante esse tempo eu migrei de engine de blog várias vezes, uma meia dúzia de vezes. Por isso é um esforço inútil achar que você vai acertar logo de primeira e gastar semanas ou meses tentando fazer o engine ideal: você vai fracassar.</p>
<p>No final, a única coisa que interessa é <strong>O CONTEÚDO</strong>. O exercício de escrever, de escrever o mínimo possível pra comunicar o máximo de informação relevante, de escrever uma história que engaja do começo ao fim, de não virar formato de TCC ou página de Wiki de documentação. Escrever uma documentação é diferente de escrever um post de blog.</p>
<p>É mais importante escrever de uma forma que você mesmo goste de ler depois do que em qualquer linguagem vai fazer sua engine.</p>
]]></content:encoded><category>blog</category><category>devops</category><category>automacao</category></item><item><title>Omarchy 2.0 - ZSH Configs</title><link>https://akitaonrails.github.io/2025/09/07/omarchy-2-0-zsh-configs/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/09/07/omarchy-2-0-zsh-configs/</guid><pubDate>Sun, 07 Sep 2025 15:00:00 -0300</pubDate><description>&lt;p&gt;Estou gostando bastante de usar &lt;a href="https://omarchy.org/"target="_blank" rel="noopener"&gt;Omarchy&lt;/a&gt; como meu desktop manager. Leia &lt;a href="https://akitaonrails.com/2025/08/29/new-omarchy-2-0-install/"target="_blank" rel="noopener"&gt;meu post anterior&lt;/a&gt; pra ver como eu instalei e fiz modificações pra servir o meu setup. Seguindo nessa linha, o Omarchy vem por padrão com shell Bash. Mas eu prefiro ZSH. Bash é perfeitamente usável, é só preferência pessoal mesmo. Tem gente que prefere Fish ou outros. Mas o Omarchy vem só com Bash por enquanto, então resolvi modificar o setup pra suportar meu ZSH.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Estou gostando bastante de usar <a href="https://omarchy.org/"target="_blank" rel="noopener">Omarchy</a> como meu desktop manager. Leia <a href="https://akitaonrails.com/2025/08/29/new-omarchy-2-0-install/"target="_blank" rel="noopener">meu post anterior</a> pra ver como eu instalei e fiz modificações pra servir o meu setup. Seguindo nessa linha, o Omarchy vem por padrão com shell Bash. Mas eu prefiro ZSH. Bash é perfeitamente usável, é só preferência pessoal mesmo. Tem gente que prefere Fish ou outros. Mas o Omarchy vem só com Bash por enquanto, então resolvi modificar o setup pra suportar meu ZSH.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/screenshot-2025-09-07_15-16-57.png" alt="Meu ZSH"  loading="lazy" /></p>
<p>Se quiser mudar pra ZSH, é fácil, só fazer isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yay -S zsh zoxide
</span></span><span class="line"><span class="cl">chsh -s <span class="k">$(</span>which zsh<span class="k">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Logout, login e pronto. Mas agora seu shell vai estar vazio. Pra baixar minha config faça isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/akitaonrails/omarchy-zsh.git ~/.config/zsh
</span></span><span class="line"><span class="cl">ln -s ~/.config/zsh/.zshrc ~/.zshrc </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tem duas coisas que precisa modificar. Em <code>.zshrc</code> remova esta linha:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>source ~/.config/zsh/mounts</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso é só pra eu checar meus mounts de NFS pro meu NAS. Pra você não serve pra nada então pode tirar.</p>
<p>A segunda coisa é criar um novo arquivo para segredos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>touch ~/.config/zsh/secrets
chmod 600 ~/.config/zsh/secrets</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É nele que vai colocar coisas como suas chaves de OpenAI ou Google Cloud ou qualquer coisa assim. Por exemplo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># ~/.config/zsh/secrets
export OPENAI_API_KEY=&#34;sk-xx-v1-xxx...xxxxx&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Assim seus segredos ficam separados das suas configurações e podemos dar <code>git push</code> pra um repositório sem se preocupar que algo possa vazar no futuro. Sempre mantenha segredos, senhas, chaves, separados localmente. Isso já está no <code>.gitignore</code> então está seguro.</p>
<p>De bônus, no <code>~/.config/init</code> adicionei suporte ao <a href="https://atuin.sh/"target="_blank" rel="noopener">Atuin</a> que é uma ferramenta que serve pra sincronizar todo histórico de comandos que fez no seu shell pra um repositório no &ldquo;cloud&rdquo; (leia a documentação, você pode criar seu próprio server local também). O repositório é encriptado, então não é inseguro, mesmo se seu histórico contenha chaves secretas nos comandos.</p>
<p>Instale assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yay -S atuin
atuin register -u &lt;seu username&gt; -e &lt;seu email&gt;
atuin import auto
atuin sync</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Logo que registrar o Atuin vai gerar uma chave de encriptação aleatório. Garanta que guardou essa chave num lugar seguro como 1password, Bitwarden ou outro cofre seguro!! Sem ela não vai conseguir decriptar numa nova instalação. Se esquecer, ainda dá pra ver antes de reinstalar, por exemplo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>atuin key</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se reinstalar sua máquina ou instalar uma nova máquina e quiser seus históricos de volta, basta instalar o Atuin e fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>atuin login -u &lt;seu username&gt;
atuin sync</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ele vai pedir sua senha e chave de encriptação e trazer todo seu histórico de volta.</p>
<p>Segundo minha config, basta digitar <code>Ctrl+R</code> pra chamar a procura do histórico (que a partir de agora vai estar num sqlite na sua máquina). Leia a <a href="https://docs.atuin.sh/"target="_blank" rel="noopener">documentação</a> pra mais funcionalidades.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/screenshot-2025-09-07_15-17-20.png" alt="Ctrl&#43;R"  loading="lazy" /></p>
<p>O DHH configurou o prompt do <strong>Starship</strong> usando o jeito antigo de variável <code>PS1</code>. Mas o Starship já tem vários prompt bem bonitos fáceis de usar como &ldquo;presets&rdquo;. Eu escolhi o Pure Prompt, que eu gosto, mas tem vários outros. Se gostar de algo mais chamativo e colorido, tem este <a href="https://starship.rs/presets/pastel-powerline"target="_blank" rel="noopener">Pastel Powerline</a>:</p>
<p><img src="https://starship.rs/presets/img/pastel-powerline.png" alt="Pastel Powerline"  loading="lazy" />
Escolha o de sua preferência na <a href="https://starship.rs/presets/"target="_blank" rel="noopener">página de Presets</a> e mude em <code>~/.config/zsh/prompt</code> no lugar desta linha:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>starship preset bracketed-segments -o ~/.config/starship.toml</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por fim, eu deixei alguns ENVs em <code>~/.config/zsh/envs</code> que funcionam pros meus testes de Ollama e coisas assim, mas é bom você saber que estão lá pra não achar estranho se seu Ollama estiver se comportando estranho, edite o arquivo <code>~/.config/zsh/envs</code> pra ter os envs de sua preferência:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># envs
export OPENAI_API_BASE=&#34;https://openrouter.ai/api/v1&#34;
export DEFAULT_MODEL=&#34;o1-mini&#34;;
export OLLAMA_API_BASE=http://127.0.0.1:11434
export OLLAMA_HOME=http://127.0.0.1:11434
export OLLAMA_MODELS=/mnt/gigachad/ollama/models</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Também modifique o arquivo <code>~/.config/zsh/aliases</code> pra ter alias que você prefira:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># aliases
alias monhd=&#34;sudo ddcutil -d 1 setvcp 60 0x12&#34;
alias mondp=&#34;sudo ddcutil -d 1 setvcp 60 0x0F&#34;

alias sgpt=&#39;docker run --rm -e OPENAI_API_BASE=${OPENAI_API_BASE} -e OPENAI_API_KEY=${OPENROUTER_API_KEY} ghcr.io/tbckr/sgpt:latest txt &#39;

alias git=&#39;nocorrect git&#39;
git config --global alias.a &#39;add&#39;
git config --global alias.ps &#39;push&#39;
git config --global alias.pl &#39;pull&#39;
git config --global alias.l &#39;log&#39;
git config --global alias.c &#39;commit -m&#39;
git config --global alias.s &#39;status&#39;
git config --global alias.co &#39;checkout&#39;
git config --global alias.b &#39;branch&#39;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Os primeiros dois aliases são pro meu setup de 2 monitores onde um dos monitores recebe sinal tanto da entrada HDMI quanto da entrada DisplayPort (tenho 2 GPUs). Se não sabia, dá pra controlar isso usando o shell. Leia <a href="https://jeancharles.quillet.org/posts/2021-08-20-How-to-use-ddcutil-to-switch-input-of-a-Dell-screen.html"target="_blank" rel="noopener">este post sobre ddcutil</a></p>
<p>Em seguida você ganha o comando <code>sgpt</code>. Omarchy já trás Docker instalado. Agora no shell você pode perguntar ao ChatGPT sem precisar ficar abrindo um navegador web pra isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sgpt &#34;qual é a sintaxe de rsync pra sincronizar dois diretórios?&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Leia <a href="https://github.com/tbckr/sgpt"target="_blank" rel="noopener">documentação do SGPT</a> pra saber outras funcionalidades ou quais LLMs você pode escolher.</p>
<p>No final temos aliases pra Git. O Omarchy já tem alias pra fazer &ldquo;g&rdquo; ser &ldquo;git&rdquo; e com este que eu adicionei você pode fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git add . # antigo
g a . # novo

git commit -m &#34;initial commit&#34; # antigo
g c &#34;initial commit&#34; # novo

git commit --amend -m &#34;initial commit&#34;
g ca &#34;initial commit&#34;

git push origin master
g ps origin master

git pull origin master
g pl origin master

git log
g l

# e assim por diante</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Mais prático e menos verbose. Adicione mais aliases conforme seu workflow em <code>~/.config/aliases</code>.</p>
<p>E acho que é isso, agora sim, meu Omarchy está organizado do jeito que eu gosto. Faça suas próprias configurações pra tudo ficar do seu jeito também!</p>
]]></content:encoded><category>omarchy</category><category>ferramentas-de-desenvolvimento</category><category>automacao</category></item><item><title>[Akitando] #47 - Entendendo "Devops" para Iniciantes em Programação (Parte 1) | Série "Começando aos 40"</title><link>https://akitaonrails.github.io/2019/04/10/akitando-47-entendendo-devops-para-iniciantes-em-programacao-parte-1-serie-comecando-aos-40/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/04/10/akitando-47-entendendo-devops-para-iniciantes-em-programacao-parte-1-serie-comecando-aos-40/</guid><pubDate>Wed, 10 Apr 2019 17:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/bwO8EZf0gLI"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Finalmente chegamos no tema final da série! Vamos falar um pouco sobre o tal do &amp;ldquo;devops&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Esta parte meio que depende dos conceitos que vimos nos últimos episódios então se você é iniciante, e ainda não assistiu os anteriores, recomendo que faça isso.&lt;/p&gt;
&lt;p&gt;No episódio de hoje quero dar um pouco do contexto histórico, dos anos 90 até os anos 2000, indo de configuração manual de máquinas, hospedagens compartilhadas, virtualização até os VPS. Vamos entender como uma coisa foi levando pra próxima.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/bwO8EZf0gLI"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Finalmente chegamos no tema final da série! Vamos falar um pouco sobre o tal do &ldquo;devops&rdquo;.</p>
<p>Esta parte meio que depende dos conceitos que vimos nos últimos episódios então se você é iniciante, e ainda não assistiu os anteriores, recomendo que faça isso.</p>
<p>No episódio de hoje quero dar um pouco do contexto histórico, dos anos 90 até os anos 2000, indo de configuração manual de máquinas, hospedagens compartilhadas, virtualização até os VPS. Vamos entender como uma coisa foi levando pra próxima.</p>
<p>Muita gente confunde muitos termos, virtualização, paravirtualização, jails, containers, hypervisors e não sabe o que é o que, então eu vou distinguir tudo pra vocês finalmente entenderem.</p>
<p>=== Script</p>
<p>Olá pessoal, Fabio Akita</p>
<p>Depois de muitas semanas, finalmente terminamos o tema de Back-end semana passada. Mas continuando na série Começando aos 40, décimo-segundo episódio já, precisamos entrar no tema de infraestrutura e devops. É bom eu já deixar claro desde já que minha formação e experiência principal é desenvolvimento de software, mas ao longo dos anos eu tive que lidar com infraestrutura várias vezes então digamos que eu tenho muitas opiniões a respeito. Mas é bom lembrar que eu nunca atuei como administrador de sistemas em tempo integral.</p>
<p>Aliás, durante esses próximos episódios você vão ver que vou ficar o tempo todo complementando “como eu já expliquei antes” e eu já expliquei mesmo, nos episódios anteriores. Então eu realmente recomendo que você tenha assistido todos os últimos 11 vídeos antes de começar esse se você ainda for iniciante. Se já é programador, vamos lá, e talvez você veja que mesmo trabalhando na área ainda tem coisas que você não sabe e, nesse caso, recomendo que tente ver os vídeos anteriores também, eu sempre escolho falar coisas que eu vejo que a maioria das pessoas não conhece, independente quantos anos já trabalha.</p>
<p>Devops é um daqueles termos chatos de definir e que você provavelmente está usando errado. É a mesma coisa com Agile. Agile não é só ter sprints ou backlogs ou rituais. Devops também não é um título, não são ferramentas, não é uma metodologia, não é uma cultura. É tudo isso e nada disso. Pois é, é uma merda mesmo esse termo, eu detesto coisas difíceis de definir. Pra entender melhor devops é mais fácil começar delineando os problemas e no final talvez chegar na conclusão.</p>
<p>(&hellip;)</p>
<p>Eu diria que se eu fosse detalhar tudo que eu gostaria sobre o tal devops, eu ficaria até o fim do semestre. Em vez disso, assim como fiz com front-end, quero dar mais um apanhado geral da linha do tempo e alguns conceitos e ferramentas. Vocês tem que entender que a área é gigantesca, você pode passar anos lidando com infraestrutura e nunca ver tudo. Por exemplo, eu nunca operei diretamente num data center, eu conheço vários dos conceitos mas o hardware moderno de data centers é uma coisa que eu só conheço na teoria, portanto eu não vou entrar muito nesse assunto. A área específica de redes é outro mundo a parte. Uma coisa é rede caseira ou de pequenas empresas, outra coisa completamente diferente é lidar com redes físicas em data centers, lidar com fibra ótica e roteadores nível “industrial” que você vai encontrar em provedores. Também existem os data centers privados em grandes corporações que usam muitas das mesmas coisas.</p>
<p>Na prática, metade do mundo de infraestrutura você nunca vai ter acesso, pelo simples fato que os hardwares e componentes nível data center você nunca vai chegar a colocar as mãos. Os tipos de componentes que temos no mundo de varejo consumidor, os PCzinhos xing lings que montamos ou mesmo os notebooks, usam componentes muito mais baratos e menos confiáveis do que você usa num data center. Apesar dos conceitos serem similares, os componentes que você tem acesso não se comparam com os de um data center. Então não é o seu CPU Core i7 ou i9 só que centenas deles num servidor. Nossos componentes de varejo desperdiçam muito espaço físico e são muito pouco confiáveis pra ficar ligados 24 por 7. Servidores sérios usam processadores com dezenas de cores que conseguem ficar ligados meses a fio sem desligar. Inclusive muitos desses componentes podem ter até menos performance se comparado com similares mais baratos que você compra no mercadolivre, mas é o trade-off de sempre: trocamos um pouco de performance por confiabilidade. Aliás, a coisa mais importante no mundo de infraestrutura é confiabilidade e segurança.</p>
<p>Nos anos 90, quando os data centers estavam começando a nascer, não era incomum muitas empresas que queriam ter presença na internet comprar seus próprios servidores ou até mesmo PCs caseiros, contratar a tal internet dedicada que era super caro porque estamos falando de uma época que internet era um troço opcional, a maioria das pessoas sequer usava ainda, e quem usava discava por modem de madrugada pra pagar menos pulsos. Felizmente a Lei de Moore estava no seu período mais acelerado e o início da Bolha da Internet do meio pro fim dos anos 90 acelerou o desenvolvimento da infraestrutura geral. Quando eu estava na faculdade acho que o Brasil tinha tipo uns 3 backbones que ligavam o país com o resto do mundo. Não era incomum ter problema num backbone tipo o da Fapesp e metade do Brasil ficava ilhado do resto do mundo. Era assim rudimentar no começo dos anos 90.</p>
<p>Quando estávamos fazendo os primeiros sites dinâmicos, com ASP ou PHP ou Perl, eu expliquei que o mais popular era rodar algo como Apache com coisas como mod_php ou Windows NT 4 com a primeira versão do IIS pra rodar ASP. Os bancos de dados não eram grande coisa e o mundo enterprise só rodava coisas como Oracle ou IBM DB2. O mundo open source tinha acabado de começar e um MySQL nem podia ser considerado um banco de dados de verdade ainda. O SQL Server estava iniciando depois que a Microsoft adquiriu a Sybase. Então as opções não eram muitas.</p>
<p>Um Apache funcionava inicialmente fazendo fork de si mesmo pra cada nova requisição. As requisições HTTP caíam em algum virtual host. Um virtual host é uma configuração do servidor Web indicando quem vai responder a requisição. Podia ser mapeando a URL pra uma pasta no disco que tem HTMLs estáticos ou podia ser mapeando a URL pra ativar algum programa CGI ou um modulo como mod_perl ou mod_php. Seja no Apache antigo ou num NGINX moderno, você sempre vai estar configurando virtual hosts.</p>
<p>Os virtual hosts ficavam num arquivo de configuração do Apache e toda vez que você mexia nesse arquivo precisava restartar o servidor pra carregar essas configurações e habilitar novos sites. Assim um único servidor Apache podia servir múltiplos sites de uma só vez. Nas faculdades era comum você ter servidores Unix como HP-UX ou Solaris com Apache e cada usuário do UNIX tinha uma pasta onde ele podia jogar arquivos que automaticamente ficavam disponíveis no servidor da faculdade. Com o tempo alguns alunos podiam ter contas nesses servidores e também compartilhar coisas no mesmo servidor. Foi assim que eu comecei a experimentar com web e aprender Unix em 1996.</p>
<p>Entendam que até o fim os anos 80 não era comum uma pessoa ter um computador inteiro só pra ela. Especialmente computadores potentes já que mesmos os caseiros dos anos 80 eram super limitados e fracos. A maioria não era muito mais potente que um nintendinho 8-bits. Por isso os grandes mainframes usavam sistemas operacionais que permitiam o tal time-sharing que já expliquei. E a geração UNIX evoluiu sendo um sistema multi-usuário. Quando nos anos 80 e 90 as comunidades de desenvolvimento quiseram tentar trazer a experiência do UNIX pra plataforma baixa, culminando no Linux, também ganhamos essas características de multi-usuários e multi-tarefa. Ou seja, no mesmo sistema operacional era possível ter múltiplos usuários guardando seus dados e documentos digitais na sua conta e seus programas não afetam o de outros usuários. Já sabemos como o sistema operacional isola os processos de afetar uns aos outros, mas também tínhamos esse isolamento no nível do usuário.</p>
<p>Somado ao fato que servidores sempre foram coisas caras, que o sistema operacional tinha a habilidade de ter múltiplos usuários, e que a maioria dos programas não roda consumindo 100% dos recursos o tempo todo, permitindo multi-tarefa preemptiva, foi natural começarmos a ter PCs montados com Linux, Apache e MySQL preparados pra rodar múltiplos sites de múltiplos usuários tudo na mesma máquina. Quando PHP começou a se popularizar, começaram a surgir vários painéis de gerenciamento pra facilitar cadastrar e gerenciar novos usuários e novos sites, produtos como o famoso cPanel criado em 1996.</p>
<p>Assim começa a era dos servidores corporativos web que sempre foram relegados a um recurso secundário frente aos gigantes ERPs como os da Oracle ou SAP. Pouca gente sabia mexer em Unix ou Linux, então painéis como cPanel tiveram um papel muito importante. Mas algumas pessoas foram um passo além: criaram um modelo de negócios onde era possível qualquer pessoa pagar uma mensalidade e ganhar um usuário numa máquina dessas e uma conta num derivado de um cPanel. Assim eles podiam criar um banco de dados restrito dentro de um MySQL, podiam configurar virtual hosts pros seus sites e podiam hospedar aplicações escritas em CGI com Perl ou PHP que era a opção mais performática. E assim se popularizou a primeira grande stack de tecnologias da web: o LAMP ou Linux, Apache, MySQL e PHP. E com uma stack aceita pela maioria, empresas puderam ser fundadas pra oferecer serviços, e assim começam a nascer as famigeradas hospedagens compartilhadas como as que você vê até hoje em empresas como Locaweb ou Godaddy.</p>
<p>O conceito é simples: dividir os recursos da máquina pra dezenas ou centenas de sites pequenos. Cada site pequeno tem pouco tráfego e só durante algumas horas do dia, então era possível apertar vários deles na mesma máquina. Até hoje temos centenas de milhares de sites funcionando assim. E com isso criou-se um ecossistema de pequenas agências e freelancers que podiam desenvolver esses sites pequenos, e foi assim que a primeira geração de sites começou a expandir rápido.</p>
<p>Por causa disso também, essa primeira geração de programadores se preocupou muito pouco com infraestrutura. Os únicos lugares onde precisava dos tais webmasters ou administradores de sistemas ou o que eu gosto de chamar: sysadmins, eram nas grandes empresas que mantinham seus próprios pequenos servidores, seja localmente no prédio da empresa ou em regime de co-location que era o aluguel de espaço e energia dentro de um data center onde você levava seu PC e instalava na internet dedicada deles, ou mesmo dentro dos provedores que forneciam o serviço de hospedagem compartilhada.</p>
<p>Nessa primeira geração, a segurança ainda era muito ruim. Na verdade, como tudo nasceu dentro de ambientes governamentais ou acadêmicos décadas atrás, você meio que podia contar com uma etiqueta e acordo de cavalheiros. Mas quando esses sistemas passaram a ser expostos na internet, isso abriu a caixa de pandora pra hackers do mundo inteiro começar a brincar. E naquela época era muito fácil hackear os servidores, já que os sysadmins da época sabiam no máximo seguir o procedimento do manual ou dos cursos pra instalar os Linux e cPanel e colocar na internet. E acredite, naquela época ou hoje, só fazer um curso não é 10% do que você realmente vai precisar no mundo real.</p>
<p>Não era difícil achar servidores pelo mundo com porta de Telnet aberta, com usuários sem senha que você podia entrar e vasculhar a máquina e era possível até ver arquivos como /etc/passwd que tinha senhas abertas usando criptografia fraca. Em poucas horas ou dias você podia invadir um servidor. A gente baixava arquivos de senhas de servidores, usava contas em outros servidores pra deixar um programinha de brute force rodando, com rainbow tables que são basicamente tabelas de palavras pré-criptografadas que podíamos só comparar com os valores criptografados nos arquivos de senha, o que é muito mais rápido do que tentar descriptografar. Em outro episódio explico mais sobre isso.</p>
<p>E no lado do servidor web tínhamos um problema. Já sabemos que um Apache funciona fazendo forks de si mesmo ou usando threads. Também sabemos que pra um Apache rodar PHP bastava rodar como CGI ou instalar coisas como um mod_php. Já sabemos como CGI puro é extremamente lento, onde cada requisição precisava subir o binário do PHP, executar o script e terminar. Então a opção do mod_php sempre foi a preferida no começo.</p>
<p>Quando viesse uma requisição, a execução desse script se dava dentro do espaço de memória do processo do Apache. Pelos virtual hosts o processo Apache sabe como responder a requisições de diferentes sites. Embora os usuários no Linux sejam isolados, o processo do Apache é compartilhado, ou seja, o Apache roda com um usuário e grupo, por exemplo chamado “apache”. Daí a pasta na home do usuário precisava dar permissão pra esse grupo e era assim que o Apache acessava os arquivos dos usuários pra servir nos virtual hosts. Entenda que o servidor web é um processo único que ouve na mesma porta 80 pra todo site que ele serve, e coisas como o nome do domínio ou sub-domínio ou sub-pasta é que diferencia de que usuário buscar os html ou php.</p>
<p>E nesse começo de web onde tanto Apache, quanto PHP quando o mod_php eram bem mal feitos, ou melhor dizendo, ingenuamente mal feitos, era muito comum descobrir um bug no num desses componentes que acabava permitindo executar coisas ou acessar dados da memória do processo que o script não deveria. E fazendo isso podíamos de um usuário chegar nos dados de outro usuário usando o Apache como ponte. Hospedagens compartilhadas passaram anos sofrendo desse problema.</p>
<p>Uma forma de limitar esse problema era não executar o php via mod_php e dentro do espaço de memória do Apache inteiro. Foram construídas outras versões do executável do php como o suphp que era servido pelo mod_suphp do Apache, que executava o script como o usuário e não como o apache. Havia a opção de chroot que mudava a raíz do root ou raíz de diretório, de tal forma que o script não conseguisse acessar o root de verdade do sistema e acabar acessando arquivos que não deveria. Depois de várias tentativas ao longo dos anos, no final a opção menos pior era rodar os scripts como FastCGI com a vantagem que diferente de CGI ele mantém o executável rodando, evitando ter que reiniciar toda hora, e ele podia rodar sob a permissão do usuário em vez da permissão mais global do Apache.</p>
<p>Eu expliquei tudo isso pra explicar que nesse período tínhamos um dilema pra resolver: o hardware e internet eram muito caros. Então você queria usar o máximo possível dos poucos recursos disponíveis. Rodar a opção mais performática como mod_php causava problemas de segurança. Mas a opção menos pior que é FastCGI usava muito mais memória porque agora você precisava ter no mínimo uma instância do PHP por usuário com sites. E em ambientes de hospedagem compartilhada você gostaria de minimizar o uso de memória pra enfiar o máximo possível de usuários por servidor. É uma grande dilema e ficamos presos nisso por muitos anos, do fim dos anos 90 e a década dos anos 2000 inteiro.</p>
<p>E isso foi um problema porque pra muita gente, essa foi a escola mais acessível pra aprender sobre web. Você pagava, sei lá, 10 reais, e tinha um espaçozinho pra jogar um site no ar. E você tinha basicamente coisas como ASP clássico e PHP, configurados em algo como FastCGI que significava que seu site era rápido mais ou menos e de vez em quando o servidor precisava matar seu processo FastCGI se tivesse pouco uso, pra conservar memória e deixar outros usuários rodarem, ou seja, de vez em quando sua requisição ficava lenta esperando um novo processo carregar.</p>
<p>Nesse caso você gostaria de fazer apps leves, com pouco código, porque quanto mais código mais demorado ia ficar. E fora isso o único jeito que se tinha na época pra subir suas apps era conectar via FTP e ficar jogando seus scripts php e imagens em alguma pasta do seu usuário onde o virtual host do apache estava configurado pra ler. Além disso você sempre estava limitado à versão do php e bibliotecas do sistema operacional que o provedor de hospedagem compartilhada oferecia. Se precisasse de alguma biblioteca binária compartilhada do sistema, teria que pedir pro suporte instalar pra você, talvez.</p>
<p>Então até a virada do século você tinha a opção barata e ultra limitada de hospedagens compartilhadas ou precisava se virar com seu próprio servidor, seja numa empresa com infraestrutura própria, o que era raro, ou colocando seu servidor em co-locations em data centers. Nesses casos precisava você mesmo configurar tudo manualmente do zero e largar uma porta de telnet, ftp ou ssh abertos pra conseguir conectar de fora via internet, numa época em que internet 24 por 7 ainda não era uma realidade pra grande maioria das pessoas. Nessa época também era muito comum você ter os papéis dos administradores de banco de dados e administradores de rede.</p>
<p>A partir do ano 2000 algumas linhas de pesquisa e experimentação começaram a dar frutos. Eu falei rapidamente do chroot que era uma forma de limitar até onde no filesystem seus processos poderiam acessar. Um conceito maior se chama “Jail” literalmente prisão e foi no FreeBSD em 2000 que começou a ser possível particionar os recursos do mesmo sistema operacional nessas jails, de forma a isolar os processos, fazendo eles pensarem que estavam sozinhos na máquina, sem ter acesso a outras jails. E ele implementou isso de tal forma que jails podiam ter seu próprio endereço IP separado.</p>
<p>Em 2001 tivemos o Linux VServer, que eram patches na kernel do Linux que permitiam algo parecido com os Jails de FreeBSD pra particionar o sistema. Esse sistema durou até 2006 mais ou menos. Novamente o objetivo era limitar o que um processo pode fazer, que recursos e quais recursos pode acessar, de tal forma que um processo mal intencionado ou bugado não conseguisse derrubar o sistema operacional inteiro.</p>
<p>Quase no fim da sua vida, em 2004 a Sun lançou os containers de Solaris. Em conceitos é também parecido com os Jails de FreeBSD ou o VServer de Linux, criando o que ele chama de zonas, que são restrições ao que se podia fazer no sistema no nível do sistema operacional. Solaris naquela época ainda era uma referência tendo recursos como zonas somado ao lendário filesystem ZFS. E no mundo Linux surgiu algo similar chamado Open VZ, o famoso Virtuozzo.</p>
<p>Jails, Containers, Zonas, nenhum deles é o que a maioria das pessoas pensa quando falamos em virtualização. Tudo isso que eu vou chamar só de containers são basicamente patches na kernel do sistema operacional para possibilitar restringir os recursos do sistema aos processos. Na verdade a gente faz a kernel literalmente mentir pros processos. Se a máquina real tem 10 cores, a kernel mente pro processo e diz que só tem 1 pra ele. Se a máquina tem 64GB de RAM, a kernel mente de novo e diz que só tem 4 GB. Se tem outros 50 processos rodando na máquina, quando o processo pedir a lista, a kernel mente e esconde todos os outros processos. Então o processo é basicamente enganado e a essa mentira da kernel damos o nome de container.</p>
<p>Mas se falarmos de virtualização de verdade, a história vai bem mais longe, pra década de 60 e os mainframes. Como já expliquei, nessa época as máquinas eram proibitivamente caras e inacessíveis e a única forma de múltiplas pessoas conseguirem usar a mesma máquina era aproveitar as diversas pausas que o sistema operacional precisa fazer como esperar um arquivo gravar ou dados serem transferidos na rede e aproveitar pra computar outra coisa de outro usuário, o conceito de time sharing. Mas os pesquisadores de mainframe foram um passo além. Quando as máquinas começaram a ficar mais poderosas, eles começaram a particionar a máquina pra rodar mais de uma kernel de cada vez, essencialmente tendo múltiplos sistemas operacionais rodando em paralelo.</p>
<p>No caso de mainframes, cada máquina virtual tinha acesso a recursos reais próprios como canais de I/O, dessa forma eles são extremamente eficientes. Nos anos 80 era praticamente impossível falar de virtualização nos computadores caseiros de 8 ou 16 bits. Simplesmente não havia nem poder computacional e nem recursos suficiente pra se pensar em rodar múltiplos sistemas operacionais na mesma máquina. Não tinha recurso suficiente male male pra rodar 2 programas de uma só vez.</p>
<p>Nos anos 90 começamos a brincar mais com esse conceito à medida que as máquinas foram ficando mais poderosas e mais acessíveis. Pense que no fim dos anos 80 as máquinas mais poderosas do mundo doméstico tinham 33Mhz e no fim dos anos 90 estávamos pra quebrar a barreira do 1Ghz. Ou seja, em uma década o poder computacional teórico ficou 30 vezes maior a um preço similar. Além disso nos anos 90 tivemos o nascimento do Linux o que abriu as portas pra desenvolvedores poderem experimentar os conceitos que já existiam no mundo dos mainframes na plataforma baixa.</p>
<p>O conceito é razoavelmente simples. Todo processo faz as tais syscalls como já expliquei. Além disso processos normais rodam num ambiente mais restrito, o tal Ring-3 que também já expliquei antes. Ou seja, um processo no Ring-3 não tem permissão pra chamar funções da CPU que só a kernel no Ring-0 tem acesso. Porém, e se fizéssemos um programa especial que por sua vez carrega seus programas? E se esse programa especial respondesse a syscalls restritas mentindo as respostas? O conceito todo de virtualizar é responder uma mentira. Por exemplo, se você tentar carregar uma kernel no Ring-3 ela não vai funcionar porque vai querer fazer chamadas de Ring-0 pra acessar o hardware real, que não existem na Ring-3.</p>
<p>Mas se esse programa especial responder a essas chamadas e mentir, a tal kernel no Ring-3 ou user land vai “acreditar” que está com acesso à máquina real. Fazendo uma tangente na filosofia isso me lembra o demônio malicioso de Descartes ou a Alegoria das Cavernas de Platão ou simplesmente o experimento do cérebro no jarro. Nosso cérebro é como se fosse um computador, e seu I/O ou input e output são nossos 5 sentidos, o tato, audição, visão, paladar e olfato. Nosso corpo humano funciona como os teclado e mouse desse computador. E se fosse possível tirar o cérebro do nosso crânio e colocar num jarro. E agora fosse possível ligar o I/O do cérebro ao de um computador, mandando sinais elétricos criados por esse computador.</p>
<p>Para o cérebro, se os sinais fizerem sentido, ele não tem como saber que não está num corpo humano de verdade, ele “acredita” que tem um corpo, só que virtual. Nós poderíamos criar mentiras no computador e enviar esses sinais direto pro cérebro. Poderíamos inventar que estamos no Havaí e mandar as imagens do cenário de praia, o cheiro da água do mar, a brisa batendo na pele, a barulho das águas batendo nas pedras e pra todos os efeitos o cérebro vai acreditar que está no Havaí. E é exatamente isso que fazemos com o tal programa especial, ele vai mentir pro processo que é o cérebro e o processo vai acreditar que está onde esse programa especial disser que ele está.</p>
<p>O sistema operacional, pra todos os efeitos e propósitos, é um supervisor de processos. Ele é responsável por gerenciar os recursos da máquina e tornar esses recursos disponíveis pros processos da forma mais eficiente possível. Se o sistema operacional é um supervisor, o que seria um supervisor de um supervisor? Que tal um hyper visor? E é isso que o tal programa especial que eu falei é: um hypervisor.</p>
<p>Temos 2 grandes tipos de hypervisors, o tipo 1 que seriam instalados no bare metal, ou seja, na máquina real de verdade, antes do sistema operacional. Exemplos disso são o VMware vSphere, Citrix XenServer, ou Microsoft Hyper-V. Os hypervisors tipo 2 são os que rodam por cima do sistema operacional, como um programa normal, por exemplo os Virtualbox, VMware Workstation, Parallels Desktop, QEMU.</p>
<p>No lado do mainframe o tipo 1 já era comum nos anos 90, mas nos computadores domésticos começamos a explorar o tipo 2 primeiro já que era o mais simples de fazer. Basicamente simular uma CPU em software, um programa especial que responde a syscalls da Ring-0, mentindo pros outros programas. Se você prestou atenção nos episódios anteriores, sabe que fazer syscalls da Ring-3 pro Ring-0, passando de userland pra kernel-space toda hora é uma coisa custosa. E como você está simulando os recursos da máquina, vai precisar toda hora se comunicar com o sistema operacional pra efetuar a operação de verdade. Portanto é impossível simular um Pentium de 100Mhz em cima de um Pentium de 100Mhz. Talvez você consiga simular um Pentium de 10Mhz em cima de um Pentium de 100Mhz. Quando você virtualiza a CPU inteira e roda outro sistema operacional inteiro por cima, você ganha o peso de dois sistemas operacionais inteiros: o Guest ou convidado que é a kernel enganada, e o Host ou hospedeiro que é o sistema de verdade por baixo e entre eles você tem o programa especial, o Hypervisor Tipo-2. É bastante peso.</p>
<p>Em 1999 tivemos dois grandes lançamentos que eu acho interessantes: estávamos vendo o nascimento da VMWare trazendo produtos pra virtualizar um Linux em cima do Windows ou vice versa, virtualizando o sistema operacional: um hypervisor tipo 2. E em paralelo víamos o lançamento de um dos filmes mais influentes daquela geração que foi Matrix. Basicamente a Matrix virtualizou nosso mundo, eu poderia dizer que a Matrix era um hypervisor tipo 1, que podia rodar um sistema operacional chamado “mundo real”. E do jeito como eu gosto de pensar, também rodava Zion. Só que havia backdoors entre os dois mundos, então era um hypervisor com bugs propositais e os processos podiam atravessar entre os dois sistemas virtualizados.</p>
<p>Deixando a nerdice de lado um pouco, a diferença entre os containers como os Jails de BSD ou OpenVZ é que você rodava os processos de forma restrita e isolada mas todos no mesmo sistema operacional. A vantagem da virtualização com hypervisors era recriar uma máquina virtual literalmente, onde um novo sistema operacional podia ser instalado e os programas rodam como se estivessem numa máquina de verdade. Obviamente a primeira solução é muito mais rápida, mas se você quisesse rodar um programa de Windows em cima de um Linux, isso só seria possível simulando o hardware, mas ao mesmo tempo seria extremamente lento.</p>
<p>Simular uma máquina inteira do zero é o que chamamos de Full Virtualization, ou seja, do ponto de vista do sistema operacional ele está na Matrix, ele não tem como saber que a máquina por baixo não é de verdade já que o hypervisor está mentindo nas syscalls pra ele. Só que a vida de um mentiroso é muito difícil, como já diria o senso comum. Você não devia mentir sozinho, em vez disso, podemos fazer um conluio, uma conspiração mesmo. No fim basta o processo rodando acreditar que está numa máquina de verdade. Então poderíamos modificar o sistema operacional pra só ele saber que está numa máquina virtual, criando um acordo entre ele e o hypervisor. Então nas syscalls mais difíceis de mentir, o sistema operacional pede ajuda direto pro hypervisor e assim as coisas ficam mais rápidas. No caso criou-se um padrão de interfaces do hypervisor que o sistema operacional tinha conhecimento. Versões de Windows e Linux foram modificadas diretamente pra acessar essas APIs e a isso demos o nome de Paravirtualização.</p>
<p>Mas o problema é conluios dependem das duas partes fazerem sua parte na mentira, e nesse caso você não podia instalar qualquer sistema operacional de qualquer versão nesses hypervisors de paravirtualização como o Xen. O sistema operacional precisava saber mentir e ter patches pra participar do conluio. A VMware tinha o padrão Virtual Machine Interface ou VMI que por um tempo passou a vir na kernel do Linux. Mas ela foi removida por conta do próximo grande acontecimento a partir de 2005 e 2006.</p>
<p>Ao mesmo tempo quase, de forma independente, a Intel e a AMD resolveram estender o conluio pra dentro do hardware da CPU. No final, é muito lento deixar o hypervisor mentir sozinho, mas se a CPU participar da mentira, ela se torna quase tão rápido quanto a verdade. A AMD lançou extensões às instruções binárias da CPU que chamou de AMD-V e a Intel lançou a mesma coisa mas chamando de VT-X, parece mentira mas são apenas 10 novas instruções que fazem o sistema operacional virtualizado achar que está na Ring-0, que é todo o objetivo dos esforços de virtualização. Essas 10 instruções aqui embaixo
(VMPTRLD, VMPTRST, VMCLEAR, VMREAD, VMWRITE, VMCALL, VMLAUNCH, VMRESUME, VMXOFF, and VMXON)</p>
<p>Essa evolução dos processadores funcionou tão bem que a performance de muitas aplicações chegou a aproximar a performance nativa sem virtualização em alguns casos. Foi em 2005 que as plataformas baixas finalmente chegaram mais próximos dos mainframes dos anos 70 que rodavam sistemas como o System/370.</p>
<p>Vamos recapitular, no meio da primeira década dos anos 2000 já começamos a ter hypervisors tipo 1 que podia rodar múltiplos sistemas operacionais em paralelo no bare metal. E também tínhamos a opção de hypervisors tipo 2 pra rodar um sistema hospedeiro leve e por cima dele rodar múltiplos sistemas operacionais também. Isso começou a se tornar extremamente importante pra data centers onde os servidores sempre foram difíceis de dar manutenção e dividir os recursos entre diferentes aplicações, de diferentes clientes, rodando na mesma máquina, com segurança e performance. Com as novas instruções VT-X e AMD-v dando performance suficiente, começou de fato o mercado de virtualização da infraestrutura.</p>
<p>De qualquer forma, voltando aos servidores dava trabalho montar um servidor novo. Houve uma época que se eu precisasse de um servidor, eu tinha que abrir uma ordem de serviço no data center da empresa ou do provedor. Eles iam levar 1 ou 2 dias, pra reservar um hardware, instalar e configurar um sistema operacional, ligar monitoramento, conectar na rede e deixar disponível pra mim. E essa máquina ia ficar a maior parte do tempo sem fazer nada, e mesmo quando estivesse fazendo nunca ia usar todos os recursos ao máximo. Por isso era tão caro: pra provisionar era caro, e a máquina era um grande desperdício na maioria do tempo.</p>
<p>Os VPS ajudaram a aliviar esses dois problemas. Como os hypervisors podem ser automatizados, agora bastava instalar o sistema operacional uma vez e fazer um snapshot, ou seja copiar os dados que compõe a instalação num arquivão, e a isso chamamos de Imagens. Como as máquinas são virtuais e são sempre as mesmas, as imagens são sempre compatíveis sem problemas de coisas como drivers. Quem tinha que se preocupar com drivers e hardware que podia mudar (tipo quando trocávamos uma placa de rede com defeito) era o sistema hospedeiro ou o hypervisor tipo 1, ou seja, só quem roda em bare metal. Portanto, via uma interface web simples e passando um número de cartão de crédito, em poucos minutos eu podia ter uma máquina virtualizada ligada na rede pronta pra usar. Foi uma evolução impressionante no começo dos anos 2000.</p>
<p>Tudo isso ajudava uma pessoa a criar uma máquina, mas ainda não ajudava tanto as grandes empresas que precisavam controlar coisas realmente complexas e distribuídas com múltiplos servidores. Quando era só uma máquina, alguém da empresa entrava nela de vez em quando pra ficar dando manutenção, instalando coisa nova e rodando atualização. Mas se você tinha 5, 10 ou 100 máquinas, precisava de uma equipe inteira pra ficar manualmente atualizando o sistema ou consertando problemas. Era tosco lidar com equipes de TI fazendo tudo na mão nessa época. A Microsoft tinha serviços no Windows Server pra tentar automatizar as coisas, mas vou te dizer que ter que lidar com Active Directory sempre foi um pé no saco e é até hoje uma das tecnologias que eu mais detestei ter que lidar, era tipo uma extensão do Registry pra servidor, ambas que só dão dores de cabeça. Eu entendo os motivos e a utilidade, mas continua sendo uma das soluções com a pior usabilidade desde a época dos mainframes.</p>
<p>Uma outra categoria de software começou a surgir, principalmente a partir de 1993 quando foi lançado pela primeira vez, ou a partir de 1998 quando saiu a versão 3 do software conhecido como CF Engine. Qualquer administrador de sistemas Linux trabalhando com vários servidores, por exemplo nos provedores de hospedagens compartilhadas como eu expliquei, usaram CF Engine 3 em algum momento. Claro, a maioria não sabia o que era isso porque as pessoas têm a tendência de não estudar ou querer fazer coisas complicadas repetidas vezes pra mostrar manja. Mas os realmente bons são preguiçosos: nós não gostamos de fazer a mesma coisa repetidas vezes e preferimos fazer um software pra fazer as coisas chatas por nós. CF Engine é isso: um estagiário de administrador de sistemas que tem as receitas pra instalar a mesma máquina do mesmo jeito todas as vezes, num único comando.</p>
<p>O CF Engine funciona com uma linguagem declarativa que descreve uma promessa. Na prática é a descrição do que o servidor deveria ter instalado (os diversos pacotes e binários) e como esses componentes deveriam ser configurados (ordens pra editar os arquivos de configuração como os que ficam no famoso diretório /etc) e quais serviços deveriam ser inicializados pelo systemd logo que a máquina inicia. O CF é um automatizador, em vez de você ter que entrar máquina a máquina e ficar digitando dezenas de comandos pra fazer tudo isso, e ficar esperando comando a comando executar; você instala no máximo um sistema operacional zerado, instala o agente do CF e faz ele conectar no servidor de CF. De lá o servidor por enviar essas promessas pra nova máquina e fazer tudo que precisa pra que fique do jeito que você precisa.</p>
<p>Avançando rapidamente na história, na sequência tivemos o Puppet lançado em 2005 e escrito em Ruby, o que possibilitou usar as capacidades de criar sub-linguagens, que chamamos de DSLs ou Domain Specific Languages pra criar o equivalente das promessas de CF. Em 2009 também em Ruby tivemos o lançamento do Chef que é o motor de produtos como o AWS Opscode por exemplo.</p>
<p>A grande vantagem de sistemas como CF Engine, Chef ou Puppet é descrever infraestrutura como código. Dessa forma, se os pré-requisitos fossem os mesmos, você podia não só mandar rodar a mesma receita várias vezes e garantidamente ter a mesma máquina no final como podia compartilhar essas receitas da mesma forma como compartilhamos código open source e a comunidade podia ajustar essas receitas, fazendo ficar melhor do ponto de vista de minimizar as coisas que eram instaladas ou como melhorar as configurações pra ficar mais seguras. Em vez de todo administrador de sistemas ter que reinventar a roda do zero, podia já partir de uma base comum.</p>
<p>Sistemas como o CF, Puppet ou Chef seguem uma arquitetura de cliente-servidor onde as novas máquinas ganham agentes que são processos que ficam rodando e escutando ordens vindas de um servidor. Assim era mais simples provisionar novas máquinas, bastava que no processo a imagem do sistema operacional viesse com o agente e quando ela plugasse na rede podia ser vista pelo servidor e iniciar o processo de enviar as receitas pra instalar o que fosse necessário. Uma coisa que muito iniciante reclamava e ainda reclama é que esses sistemas tinham pelo menos 2 componentes: os agentes e o servidor, então tinha bastante coisa pra configurar antes de ter a primeira máquina automaticamente configurada. Se seu trabalho era configurar dezenas de máquinas, tudo bem, mas se você só queria configurar uma única máquina, era mais trabalho do que você gostaria de ter.</p>
<p>Antes desses sistemas ficarem mais populares o mais comum era algum estagiário manualmente instalar o sistema operacional, configurar a rede e plugar a máquina rede, deixando pelo menos o Telnet ou mais recentemente o servidor de SSH configurado. Pra quem não sabe, a porta 23 sempre foi do Telnet que é uma forma de acessar o terminal do servidor remotamente. Telnet é o teletype network e na prática você podia se logar remotamente da sua máquina num servidor Unix ou Linux.</p>
<p>Esse protocolo é antigo, do fim dos anos 60 então quando a internet começou a se popularizar ficou bem claro que era muito inseguro. Se você não sabe disso é muito simples fazer uma coisa chamada sniffing que é ouvir os pacotes TCP trafegando na rede e ver o que tem dentro deles, se o dado estiver aberto você pode facilmente interceptar e ler esses pacotes. Protocolos de TCP como Telnet, FTP, SMTP, HTTP todos são muito fáceis de sniffar e por isso viemos gradativamente substituindo o uso deles por outros mais seguros como o SSH, SFTP, SMTP com TLS, e HTTPS. SSH que todo mundo conhece hoje em dia e é exposto na porta 22.</p>
<p>Assim como Telnet, você pode abrir um terminal remoto interativo via SSH e ficar digitando comandos manualmente. Ou você pode automatizar o SSH e usar ele como um túnel. Ou seja, você pode criar um programa que envia comandos ao cliente de SSH sem que você precise abrir o terminal interativo; como um programa de linha de comando. Em cima desse conceito, podíamos criar arquivos de texto com vários comandos que queremos rodar no servidor. O SSH funciona também pra transferir arquivos de forma encriptada como um FTP melhorado. Então podemos mandar um script de comandos pro servidor e depois mandar executar esse script no servidor.</p>
<p>Se você pensou 2 segundos, isso possibilita outra coisa: automatizar a instalação e configuração de novos servidores. Todo novo sistema operacional de servidor que se preza já deixa pré-instalado um servidor como o OpenSSH. Com CF, Chef ou Puppet precisamos deixar também instalados os seus programas agentes. Mas só com SSH já dá pra fazer muita coisa. E assim em 2006 surge outra ferramenta do mundo Ruby, o Capistrano que antes se chamava SwitchTower. É um sistema de deployment sem agentes ou agentless. O agente no caso seria o próprio servidor de SSH. Com comandos simples você pode instalar sua aplicação em múltiplos servidores, configurados todos iguais porque você estava automatizando os mesmos comandos pra rodar em todos eles, e de quebra ele podia também configurar seu servidor de banco de dados, seu balanceador de carga e tudo mais, com a vantagem de ser mais simples que CF ou Puppet.</p>
<p>Se você que começou nos anos recentes pensa que o Ansible que foi lançado em 2015 foi uma grande novidade, na verdade ele foi só uma evolução do Fabric de Python que saiu antes dele e que por sua vez foi um clone de Capistrano. Em linhas gerais temos os gerenciadores de configuração de infraestrutura, mais complicados, que são cliente-servidor como CF, Puppet e Chef e os que são mais simples e agentless como Capistrano e Ansible. Eu diria que ambos servem propósitos diferentes pra públicos diferentes. Se você é um desenvolvedor de aplicações web que precisa subir uma aplicação simples numa máquina virtual num VPS e depois conseguir fazer deployments automatizados rápidos das atualizações e correções, você deveria usar Ansible.</p>
<p>Por outro lado, se você precisava manter 10 máquinas web, 3 máquinas de bancos de dados, 1 servidor de cache e coisas assim, era melhor organizar sua infraestrutura em conjuntos de receitas usar coisas como Chef pra criar e atualizar essas máquinas. As pessoas pensam que infraestrutura é só instalar a máquina, colocar na rede e acabou. Longe disso, você precisa atualizar o sistema operacional com correções de segurança, você precisa subir atualização das aplicações que rodam nelas. Mais importante: se uma máquina é invadida ou mesmo dá algum problema inexplicável, não vale a pena tentar consertar, você precisa preparar a arquitetura pra que seja possível destruir a máquina por completo e recriar uma nova pra colocar no lugar. Manualmente é um trabalho de corno e você vai errar muitas vezes esquecendo de instalar ou configurar alguma coisa, mas automatizado você sempre garante que a máquina vai ser montada da forma certa.</p>
<p>Essas ferramentas de automatização não só precisavam instalar máquinas mas também precisavam garantir que as premissas de segurança estão sendo cumpridas. Que as políticas certas estão aplicadas, que as permissões certas de usuários foram feitas, que as máquinas estão nas vlans corretas e que as regras de firewall também estão corretas. Esse conhecimento veio sendo automatizado também.</p>
<p>Agora pense assim, o começo do século XXI foi extremamente caótico com a bolha da internet que explodiu em 2001. Passamos uns 2 ou 3 anos de depressão, sem nada muito interessante realmente acontecendo. Mas então a controvérsia do Napster vs a indústria da música, o lançamento do iPod e iTunes, começou a trazer o entretenimento de massa pra internet, bandas independentes começaram a se lançar via internet e a hegemonia da MTV como influenciador começou a desaparecer quando as redes sociais de primeira geração, como MySpace, começaram a tomar a internet de assalto. Se você não tem noção desse efeito pense que em 2006 MySpace foi mais visitado do que o próprio Google, consegue imaginar isso?</p>
<p>Já em 2008 o Facebook destronou o MySpace, então imaginem dois titãs em disputa. 2008 que eu já expliquei que foi a época da maior crise econômica dos tempos modernos mas no lado da internet as coisas estavam esquentando absurdamente, principalmente em Silicon Valley. Foi em 2005 que o Netflix pivotou pra oferecer vídeo streaming em vez de entregar DVD, foi em 2006 que o Google comprou o YouTube, em 2007 que nasceu Twitter, em 2008 que Facebook despontou como a liderança das redes sociais, também em 2008 que a Apple inaugurou o iPhone App Store, em 2009 que surgiu o Uber, em 2010 que surgiu o monstro Groupon, que já praticamente morreu. Ou seja do meio pro final dos anos 2000 tivemos uma aceleração abrupta que não se via desde o fim dos anos 90. E é por isso que esse foi o período que mais tivemos evolução nas tecnologias.</p>
<p>Diferente dos anos 90, agora estávamos num ponto onde as pessoas comuns já tinham acesso à internet em casa e partir dos anos 2010 começamos a colocar a internet no bolso das pessoas, acesso de qualquer lugar, a qualquer momento. E com o advento dos smartphones e das redes sociais criamos uma massa gigantesca de pessoas que querem ficar online o tempo todo, e mesmo tendo coisas como ecommerce antes, a escala que os unicórnios atingiram exigiam muito. Ecommerces tem horário e dias pra picos. Redes sociais tem picos o tempo todo. Instalar máquinas na mão dentro de uma Netflix seria completamente inviável. Por isso muitas das novas tecnologias vieram exatamente desses unicórnios. Pra piorar a partir de 2011 ainda começamos a adotar mais e mais tecnologias que ajudavam a manter as pessoas online mais tempo, como WebRTC pra comunicações e WebSockets.</p>
<p>Até então a internet era feita pra ser desconectada. Você podia carregar um site com seus conteúdos, desconectar da internet e continuar lendo. Não era prático, mas o protocolo HTTP foi feito pra não ter estado. Você baixa, renderiza no navegador e acabou. Mas com WebRTC e WebSockets você passou a gerenciar estados e precisa estar online pra tudo funcionar, coisas como Messenger, Slack, Whatsapp, só fazem sentido num ambiente onde as pessoas estão online ao mesmo tempo na maior parte do tempo.</p>
<p>Entenda uma coisa importante: a grande maioria das boas práticas e recomendações que você vê na internet hoje vinda de engenheiros de um Netflix, de um Twitter, de um Facebook, ou de um Google, só fazem sentido pra eles na maior parte do tempo. Ouça isso com muita atenção: você não é o Netflix, tá muito longe. O Netflix levou anos pra ser o Netflix. Os unicórnios são chamados de unicórnios porque são quase impossíveis se você tentar replicar e precisam de vários fatores incontroláveis incluindo estar no lugar certo na hora certa, pra acontecer. Portanto não é porque você usa uma tecnologia do Google que vai te levar um pouco mais próximo de ser um mini Google. Não existe correlação, esqueça essas fantasias.</p>
<p>Portanto não, você não deveria usar um Apache Mesos, tecnologia criada pelo Twitter, na sua infraestrutura de 3 máquinas na sua micro-empresa que você gosta de chamar de tech startup. Toda vez que você ouvir o argumento “devíamos usar isso porque o Twitter usa” ignore completamente essa pessoa, ela não tem idéia do que está falando e é uma pessoa perigosa.</p>
<p>Isso tudo dito o fato é que de repente começaram a surgir empresas que estavam escalando absurdamente rápido num ambiente onde a maioria dos data centers disponíveis não sabiam como atender essa demanda. Era muito demorado manualmente instalar e configurar máquinas. Mesmo com ferramentas como CF, Chef eles estavam atingindo limites. Pense assim: num ambiente cliente e servidor, onde de repente você tem mil máquinas batendo num servidor de configurações como o CF e você vai engargalar a rede se precisar baixar um binário grande pra mil máquinas na rede interna, vai começar a engargalar o servidor de configurações. Novas arquiteturas precisaram ser criadas.</p>
<p>Estamos chegando na era mais acelerada do mundo de infraestrutura web que eu argumentaria que foi a partir de 2006 até pelo menos 2015. Se você acompanhou os últimos episódios sabe o quanto de coisas saiu nessa década e se não assistiu agora é a hora de ver porque chegamos ao final deste episódio. No próximo episódio quero contar como as tecnologias que mencionei hoje vão culminar em coisas como Docker ou Kubernetes que você tanto ouve falar em tudo que é blog ou evento de tecnologia.</p>
<p>Eu espero que vocês tenham ganhado um contexto melhor quanto ouvirem sobre essas tecnologias. Eu falei de muitos conceitos então é natural que vocês tenham dúvidas, não deixem de mandar nos comentários pra galera poder ajudar também. Se curtiram o vídeo mandem um joinha, compartilhem com seus amigos, assinem o canal e não esqueçam de clicar no sininho pra não perder os próximos episódios. A gente se vê semana que vem, até mais!</p>
]]></content:encoded><category>devops</category><category>automacao</category><category>cloud</category><category>akitando</category></item><item><title>Substituindo rvm, rbenv, nvm e outros pelo asdf</title><link>https://akitaonrails.github.io/2017/10/24/substituindo-rvm-rbenv-nvm-e-outros-pelo-asdf/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/10/24/substituindo-rvm-rbenv-nvm-e-outros-pelo-asdf/</guid><pubDate>Tue, 24 Oct 2017 14:19:00 -0200</pubDate><description>&lt;p&gt;Muita gente está usando Docker como meio de ter diferentes versões de Ruby ou de qualquer outra linguagem. Ainda acho que o overhead adicional tanto em uso de recursos quanto em fricção de usabilidade simplesmente não compensa. Recomendo fortemente contra isso. Docker é ótimo como base para infraestruturas automatizadas, mas prefiro tê-lo apenas em servidores.&lt;/p&gt;
&lt;p&gt;Faz bastante tempo que uso o &lt;a href="https://github.com/asdf-vm/asdf"target="_blank" rel="noopener"&gt;asdf&lt;/a&gt; como meu gerenciador principal de versões de Ruby, e estou confiante de que posso recomendá-lo no lugar dos mais conhecidos RVM ou Rbenv.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Muita gente está usando Docker como meio de ter diferentes versões de Ruby ou de qualquer outra linguagem. Ainda acho que o overhead adicional tanto em uso de recursos quanto em fricção de usabilidade simplesmente não compensa. Recomendo fortemente contra isso. Docker é ótimo como base para infraestruturas automatizadas, mas prefiro tê-lo apenas em servidores.</p>
<p>Faz bastante tempo que uso o <a href="https://github.com/asdf-vm/asdf"target="_blank" rel="noopener">asdf</a> como meu gerenciador principal de versões de Ruby, e estou confiante de que posso recomendá-lo no lugar dos mais conhecidos RVM ou Rbenv.</p>
<p>Além disso, ele não gerencia só Ruby — ele gerencia praticamente todas as linguagens que você precisar, com um único conjunto de comandos. Então nem precisa mais de virtualenv para Python ou NVM para Node.js. É só usar o ASDF.</p>
<p>A instalação não poderia ser mais simples. Basta seguir o <a href="https://github.com/asdf-vm/asdf/blob/master/README.md"target="_blank" rel="noopener">README</a> na página do projeto.</p>
<p>Não esqueça de instalar as ferramentas base de desenvolvimento para o seu ambiente. Você pode seguir a <a href="https://github.com/rbenv/ruby-build/wiki"target="_blank" rel="noopener">wiki do ruby-build</a> como referência:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>## Para OS X
# opcional, mas recomendado:
brew install openssl libyaml libffi

# obrigatório para compilar Ruby &lt;= 1.9.3-p0:
brew tap homebrew/dupes &amp;&amp; brew install apple-gcc42

## Para Ubuntu
sudo apt-get install gcc-6 autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm3 libgdbm-dev

## Para Arch
sudo pacman -S --needed gcc5 base-devel libffi libyaml openssl zlib</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois instale o próprio ASDF:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.4.0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Então adicione a configuração de ambiente para o PATH e o autocomplete:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Para Ubuntu ou outras distros Linux
echo -e &#39;\n. $HOME/.asdf/asdf.sh&#39; &gt;&gt; ~/.bashrc
echo -e &#39;\n. $HOME/.asdf/completions/asdf.bash&#39; &gt;&gt; ~/.bashrc

# OU para Mac OSX
echo -e &#39;\n. $HOME/.asdf/asdf.sh&#39; &gt;&gt; ~/.bash_profile
echo -e &#39;\n. $HOME/.asdf/completions/asdf.bash&#39; &gt;&gt; ~/.bash_profile</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por fim, você precisa instalar um dos dezenas de <a href="https://github.com/asdf-vm/asdf-plugins"target="_blank" rel="noopener">plugins</a> disponíveis. No meu caso, tenho estes instalados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf plugin-add clojure https://github.com/vic/asdf-clojure.git
asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
asdf plugin-add golang https://github.com/kennyp/asdf-golang.git
asdf plugin-add python https://github.com/tuvistavie/asdf-python.git
asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git

# Importa as chaves OpenPGP do time de releases do Node.js para o keyring principal
bash ~/.asdf/plugins/nodejs/bin/import-release-team-keyring

asdf plugin-add nodejs https://github.com/asdf-vm/asdf-nodejs.git</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Para atualizar todos os plugins de uma vez, basta rodar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf plugin-update --all</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Para ver as versões disponíveis de uma linguagem específica:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf list-all ruby
asdf list-all clojure
asdf list-all python</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Para instalar qualquer versão que você precisar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf install ruby 2.4.2
asdf install nodejs 8.7.0
asdf install erlang 20.1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois de instalar uma versão, costumo definir uma como padrão global do sistema:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf global ruby 2.4.2
asdf global elixir 1.5.2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E dentro de um diretório de projeto específico, posso definir outra versão, válida só para aquele projeto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf local ruby 2.3.4</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse comando grava um arquivo <code>.tool-versions</code> no diretório onde você está quando o executa. Ele contém a linguagem e a versão escolhidas, então sempre que você voltar para aquele diretório o ASDF vai configurar a versão correta automaticamente. O <code>asdf global &lt;linguagem&gt;</code> que mencionei antes faz a mesma coisa, só que grava o <code>.tool-versions</code> no seu diretório home. A configuração local sobrescreve a do home.</p>
<p>Outro ponto importante: sempre que você instalar bibliotecas que têm scripts executáveis que precisam estar no PATH, você deve fazer o <strong>reshim</strong> deles. Por exemplo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>npm install -g phantomjs # instala o phantomjs
asdf reshim nodejs # coloca o shim do executável phantomjs no PATH
phantomjs # executa corretamente</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você tentar instalar versões de Ruby anteriores à 2.4, vai encontrar <a href="https://github.com/asdf-vm/asdf-ruby/wiki/Ruby-Installation-Problems"target="_blank" rel="noopener">problemas de compilação</a>, pois elas dependem do gcc5 e do openssl-1.0. Nesse caso, use o seguinte comando (assumindo que você já instalou o openssl-1.0 e o gcc5 obsoletos):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>CC=gcc-5 PKG_CONFIG_PATH=/usr/lib/openssl-1.0/pkgconfig \
RUBY_EXTRA_CONFIGURE_OPTIONS=&#34;--with-openssl-dir=/usr/lib/openssl-1.0&#34; \
asdf install ruby 2.3.4</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Às vezes, quando uma dependência está faltando e a instalação falha, você precisa remover manualmente a versão antes de tentar reinstalar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf remove &lt;linguagem&gt; &lt;versão&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por fim, se você costumava adicionar algo ao seu bash ou zsh para mostrar a versão atual da linguagem na linha de comando — provavelmente usando algo como <code>rvm-prompt</code> — no caso do ASDF você vai precisar de algo um pouco mais longo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf current ruby | awk -F&#39; &#39; &#39;{print $1}&#39;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso extrai só a versão (o comando <code>asdf current</code> mostra também qual arquivo <code>.tool-versions</code> está sendo usado, por isso o resultado é mais longo).</p>
<p>Fora isso, está tudo pronto. Um gerenciador de versões para governar todos. Sem RVM, sem Virtualenv, sem NVM. Pode viver feliz para sempre!</p>
]]></content:encoded><category>ferramentas-de-desenvolvimento</category><category>automacao</category></item><item><title>Hackeando o Mattermost Team Edition</title><link>https://akitaonrails.github.io/2016/08/12/hackeando-o-mattermost-team-edition/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/08/12/hackeando-o-mattermost-team-edition/</guid><pubDate>Fri, 12 Aug 2016 14:43:00 -0300</pubDate><description>&lt;p&gt;No &lt;a href="http://www.akitaonrails.com/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener"&gt;meu post anterior&lt;/a&gt; contei sobre minha migração do Slack para o Rocket.chat. Mas também mencionei que, antes do Rocket.chat, eu preferiria usar o Mattermost. Primeiro, porque é escrito em Go (leve, altamente concorrente, super estável), e segundo, porque a base de código mostra muito mais qualidade do que a do Rocket.chat (que parece super frágil, com praticamente nenhum teste automatizado).&lt;/p&gt;
&lt;p&gt;Mas minha maior reclamação com o Mattermost é que a Team Edition, gratuita e open source, não tem um recurso super importante: impedir que usuários apaguem grupos privados.&lt;/p&gt;</description><content:encoded><![CDATA[<p>No <a href="http://www.akitaonrails.com/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener">meu post anterior</a> contei sobre minha migração do Slack para o Rocket.chat. Mas também mencionei que, antes do Rocket.chat, eu preferiria usar o Mattermost. Primeiro, porque é escrito em Go (leve, altamente concorrente, super estável), e segundo, porque a base de código mostra muito mais qualidade do que a do Rocket.chat (que parece super frágil, com praticamente nenhum teste automatizado).</p>
<p>Mas minha maior reclamação com o Mattermost é que a Team Edition, gratuita e open source, não tem um recurso super importante: impedir que usuários apaguem grupos privados.</p>
<p>O <a href="http://www.akitaonrails.com/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough#comment-2832915684"target="_blank" rel="noopener">@iantien comentou</a> que na verdade os grupos privados nunca são realmente &ldquo;apagados&rdquo;, eles só ficam marcados como deletados, auditados, mas todos os dados continuam no banco. Só que a UI não tem como esconder a opção de &ldquo;delete&rdquo; dos usuários e não existe uma tela de Administração para desarquivar os grupos apagados.</p>
<p>De fato, dá para abrir uma sessão <code>psql</code> no seu banco PostgreSQL e simplesmente rodar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>update channels set deleteat = 0;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso vai desarquivar e restaurar todos os canais apagados. Mas dá para ver como isso é um saco.</p>
<p>Discordo fortemente quando ele diz que só &ldquo;empresas com 10.000 usuários&rdquo; precisariam de um recurso desses. Mesmo em um time pequeno, qualquer usuário mal-humorado pode simplesmente arquivar um canal do nada e atrapalhar toda a comunicação do time nos grupos privados. Claro, a &ldquo;Town Square&rdquo; e outros canais públicos continuam funcionando, mas se você tem até mesmo um único usuário externo participando de projetos, por exemplo, você quer usar grupos privados para isolar a comunicação interna dos usuários externos.</p>
<p>Então, não ter a opção de permissões bem básicas (como impedir que membros apaguem canais ou grupos privados) é um baita empecilho mesmo para times pequenos. E sim, a taxa de USD 20/usuário/ano não é cara, mas como o Mattermost ainda tem menos recursos que o Rocket.chat, fica uma proposta difícil de vender.</p>
<p>Hackear o código diretamente e adicionar uma flag para desabilitar essa opção, em Go, até que é bem fácil, só que você teria que manter seu próprio fork (porque acho que o Mattermost não aceitaria um pull request de uma feature que já está na oferta paga, Enterprise, deles).</p>
<p>Mas depois que o @iantien comentou que nada é apagado e tudo fica auditado, rapidamente percebi que eu poderia usar os metadados de auditoria e bolar um jeito de restaurar automaticamente os canais (exceto quando for o system admin fazendo). Tudo isso sem mexer no código-fonte.</p>
<p>Dá para usar as várias ferramentas disponíveis no próprio PostgreSQL, a saber: <strong>TRIGGERS</strong>. Então, sem mais delongas, basta rodar isto no seu banco do Mattermost:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="k">REPLACE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">undelete_channel</span><span class="p">()</span><span class="w"> </span><span class="k">RETURNS</span><span class="w"> </span><span class="k">trigger</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="err">$$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">DECLARE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">user_counter</span><span class="w"> </span><span class="nb">integer</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">channel_id</span><span class="w"> </span><span class="nb">character</span><span class="w"> </span><span class="nb">varying</span><span class="p">(</span><span class="mi">26</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">BEGIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">-- Apenas para operações de delete de canal
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">IF</span><span class="w"> </span><span class="k">NEW</span><span class="p">.</span><span class="n">action</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;%/channels/%/delete&#39;</span><span class="w"> </span><span class="k">THEN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">END</span><span class="w"> </span><span class="k">IF</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">-- Verifica se é o system_admin
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">user_counter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">NEW</span><span class="p">.</span><span class="n">userid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">AND</span><span class="w"> </span><span class="n">roles</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;system_admin&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">IF</span><span class="w"> </span><span class="n">user_counter</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">THEN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">END</span><span class="w"> </span><span class="k">IF</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">channel_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">split_part</span><span class="p">(</span><span class="k">NEW</span><span class="p">.</span><span class="n">action</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;/&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">7</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">UPDATE</span><span class="w"> </span><span class="n">channels</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">SET</span><span class="w"> </span><span class="n">deleteat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">channel_id</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">END</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="err">$$</span><span class="w"> </span><span class="k">LANGUAGE</span><span class="w"> </span><span class="n">plpgsql</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">DROP</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">undelete_channel</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">audits</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="n">undelete_channel</span><span class="w"> </span><span class="k">AFTER</span><span class="w"> </span><span class="k">INSERT</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">audits</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">FOR</span><span class="w"> </span><span class="k">EACH</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">EXECUTE</span><span class="w"> </span><span class="k">PROCEDURE</span><span class="w"> </span><span class="n">undelete_channel</span><span class="p">();</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É isso, ela vai escutar novos inserts na tabela de audits, checar se é uma ação de &ldquo;channel delete&rdquo;, checar se não é um &lsquo;system_admin&rsquo;, e, sendo o caso, pega automaticamente o id do canal da URL REST da action e faz o UPDATE para trazê-lo de volta.</p>
<p>Já testei e na minha UI os usuários nem percebem que algo aconteceu. Nem o próprio usuário que tentou deletar vê o canal sumir, ele volta instantaneamente.</p>
<p>Então, se isso era a única coisa impedindo você de usar a Team Edition on-premise, gratuita, aí está. E com isso você pode derivar funções para também impedir que canais sejam renomeados, mas vou deixar isso como exercício para você (por favor, compartilhe nos comentários abaixo se fizer).</p>
<p>Happy Hacking!</p>
]]></content:encoded><category>bancos-de-dados</category><category>automacao</category></item><item><title>Ex Manga Downloadr - Parte 3: Suporte ao Mangafox!</title><link>https://akitaonrails.github.io/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox/</link><guid isPermaLink="true">https://akitaonrails.github.io/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox/</guid><pubDate>Wed, 02 Dec 2015 16:27:00 -0200</pubDate><description>&lt;p&gt;Eu achava que a &lt;a href="http://www.akitaonrails.com/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener"&gt;Parte 2&lt;/a&gt; seria meu último artigo sobre essa ferramentinha, mas no fim das contas é divertido demais para largar tão rápido. Como sempre, todo o código fonte está no meu &lt;a href="https://github.com/akitaonrails/ex_manga_downloadr"target="_blank" rel="noopener"&gt;repositório do Github&lt;/a&gt;. E o resumo do post é que agora dá pra fazer isso:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;pre&gt;&lt;code&gt;git pull
mix escript.build
./ex_manga_downloadr -n onepunch -u http://mangafox.me/manga/onepunch_man/ -d /tmp/onepunch -s mangafox&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;E pronto: download direto do Mangafox embutido! \o/&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu achava que a <a href="http://www.akitaonrails.com/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener">Parte 2</a> seria meu último artigo sobre essa ferramentinha, mas no fim das contas é divertido demais para largar tão rápido. Como sempre, todo o código fonte está no meu <a href="https://github.com/akitaonrails/ex_manga_downloadr"target="_blank" rel="noopener">repositório do Github</a>. E o resumo do post é que agora dá pra fazer isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git pull
mix escript.build
./ex_manga_downloadr -n onepunch -u http://mangafox.me/manga/onepunch_man/ -d /tmp/onepunch -s mangafox</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E pronto: download direto do Mangafox embutido! \o/</p>
<p>Tudo começou quando eu quis baixar um mangá que não existe no MangaReader mas está disponível no Mangafox.</p>
<p>Então a empreitada inicial foi copiar os módulos do parser do MangaReader (IndexPage, ChapterPage e Page) e colar numa pasta específica &ldquo;lib/ex_manga_downloadr/mangafox&rdquo;. A mesma coisa na pasta de testes unitários. Só copiar e colar os arquivos e trocar o nome do módulo &ldquo;MangaReader&rdquo; por &ldquo;Mangafox&rdquo;.</p>
<p>Claro que os formatos de URL são diferentes, os seletores CSS do Floki são um pouquinho diferentes, então é isso que precisa mudar no parser. Por exemplo, é assim que eu faço o parsing dos links de capítulo da página principal no MangaReader:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#listing a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span> <span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="o">[</span><span class="p">{</span><span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="n">url</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">url</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E essa é a mesma coisa, só que para o Mangafox:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">html</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;.chlist a[class=&#39;tips&#39;]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span> <span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="o">[</span><span class="p">{</span><span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="n">url</span><span class="p">},</span> <span class="p">{</span><span class="s2">&#34;title&#34;</span><span class="p">,</span> <span class="n">_</span><span class="p">},</span> <span class="p">{</span><span class="s2">&#34;class&#34;</span><span class="p">,</span> <span class="s2">&#34;tips&#34;</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">url</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Exatamente a mesma lógica, mas a estrutura de pattern matching muda porque os nodes do DOM HTML retornados são diferentes.</p>
<p>Outra diferença é que o MangaReader devolve tudo em texto puro por padrão, enquanto o Mangafox devolve tudo Gzipado independente de eu mandar o header HTTP &ldquo;Accept-Encoding&rdquo; (curiosamente, se eu tento várias vezes ele muda de comportamento e às vezes manda texto puro).</p>
<p>O que eu fiz de diferente foi checar se a estrutura <tt>%HTTPotion.Response{}</tt> retornada tinha o header &ldquo;Content-Encoding&rdquo; setado como &ldquo;gzip&rdquo; e, se sim, descomprimir usando o pacote &ldquo;zlib&rdquo; embutido do Erlang (sem precisar importar nada!):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">gunzip</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">headers</span><span class="o">[</span><span class="ss">:&#34;Content-Encoding&#34;</span><span class="o">]</span> <span class="o">==</span> <span class="s2">&#34;gzip&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:zlib</span><span class="o">.</span><span class="n">gunzip</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="n">body</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Eu teria preferido que o HTTPotion já fizesse isso pronto pra mim (#OpportunityToContribute!), mas foi fácil o suficiente.</p>
<p>Depois que os testes unitários começaram a passar corretamente, com o scrapper (requisições HTTPotion) e o parser (seletores Floki) ajustados, chegou a hora de fazer meu Worker reconhecer a existência desse novo conjunto de módulos.</p>
<p>O módulo Workflow só chama o Worker, que por sua vez faz o trabalho pesado de buscar páginas e baixar imagens. O Worker chamava o módulo MangaReader diretamente, assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">PoolManagement</span><span class="o">.</span><span class="n">Worker</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">GenServer</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">Logger</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">chapter_page</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Task</span><span class="o">.</span><span class="n">async</span> <span class="n">fn</span> <span class="o">-&gt;</span> 
</span></span><span class="line"><span class="cl">      <span class="ss">:poolboy</span><span class="o">.</span><span class="n">transaction</span> <span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">fn</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="p">{</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">},</span> <span class="vi">@timeout_ms</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span><span class="p">,</span> <span class="vi">@transaction_timeout_ms</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">handle_call</span><span class="p">({</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="no">ChapterPages</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">),</span> <span class="n">state</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aquele &ldquo;<tt>use ExMangaDownloadr.MangaReader</tt>&rdquo; lá em cima é só uma macro que cria os aliases para os módulos correspondentes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">__using__</span><span class="p">(</span><span class="n">_opts</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">IndexPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">ChapterPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">Page</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Então quando eu chamo &ldquo;<tt>ChapterPages.pages(chapter_link)</tt>&rdquo; é um atalho pra usar o nome qualificado completo do módulo, tipo: &ldquo;<tt>ExMangaDownloadr.MangaReader.ChapterPages.pages(chapter_link)</tt>&rdquo;.</p>
<p>Um namespace de módulo Elixir é só um Atom. Nomes de módulos aninhados têm o nome completo separado por pontos, prefixado com seu pai. Por exemplo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Foo</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmodule</span> <span class="no">Bar</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">defmodule</span> <span class="no">Xyz</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">       <span class="k">def</span> <span class="nf">teste</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">       <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pode chamar &ldquo;<tt>Foo.Bar.Xyz.teste()</tt>&rdquo; e pronto. Mas tem uma pequena pegadinha. O Elixir também prefixa transparentemente o nome completo do módulo com &ldquo;Elixir&rdquo;. Então na real, o nome completo do módulo é &ldquo;Elixir.Foo.Bar.Xyz&rdquo;, pra garantir que nenhum módulo Elixir conflite com algum módulo Erlang existente.</p>
<p>Isso é importante por causa dessa nova função que eu adicionei primeiro no módulo Worker:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">manga_source</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">module</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">source</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mangareader&#34;</span> <span class="o">-&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p">(</span><span class="s2">&#34;Elixir.ExMangaDownloadr.MangaReader.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mangafox&#34;</span>    <span class="o">-&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p">(</span><span class="s2">&#34;Elixir.ExMangaDownloadr.Mangafox.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É assim que eu mapeio de &ldquo;mangafox&rdquo; pro novo namespace &ldquo;ExMangaDownloadr.Mangafox.&rdquo;. E por causa da natureza dinâmica e baseada em troca de mensagens do Elixir, eu consigo trocar esse código:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">handle_call</span><span class="p">({</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="no">ChapterPages</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">),</span> <span class="n">state</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por esse:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">handle_call</span><span class="p">({</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">,</span> <span class="n">source</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">links</span> <span class="o">=</span> <span class="n">source</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">manga_source</span><span class="p">(</span><span class="s2">&#34;ChapterPage&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">apply</span><span class="p">(</span><span class="ss">:pages</span><span class="p">,</span> <span class="o">[</span><span class="n">chapter_link</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="n">links</span><span class="p">,</span> <span class="n">state</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora eu posso escolher entre os módulos &ldquo;Elixir.ExMangaDownloadr.Mangafox.ChapterPage&rdquo; ou &ldquo;Elixir.ExMangaDownloadr.MangaReader.ChapterPage&rdquo;, chamar a função <tt>pages/1</tt> e mandar o mesmo argumento de antes. Só preciso garantir que consigo receber a string &ldquo;source&rdquo; pela linha de comando agora, então mudo o módulo CLI assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">parse_args</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">parse</span> <span class="o">=</span> <span class="no">OptionParser</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">args</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">switches</span><span class="p">:</span> <span class="o">[</span><span class="nb">name</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">url</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">directory</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="ss">:string</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">aliases</span><span class="p">:</span> <span class="o">[</span><span class="ss">n</span><span class="p">:</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">u</span><span class="p">:</span> <span class="ss">:url</span><span class="p">,</span> <span class="ss">d</span><span class="p">:</span> <span class="ss">:directory</span><span class="p">,</span> <span class="ss">s</span><span class="p">:</span> <span class="ss">:source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">parse</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="o">[</span><span class="nb">name</span><span class="p">:</span> <span class="n">manga_name</span><span class="p">,</span> <span class="ss">url</span><span class="p">:</span> <span class="n">url</span><span class="p">,</span> <span class="ss">directory</span><span class="p">:</span> <span class="n">directory</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="n">source</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">source</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="o">[</span><span class="nb">name</span><span class="p">:</span> <span class="n">manga_name</span><span class="p">,</span> <span class="ss">directory</span><span class="p">:</span> <span class="n">directory</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="p">}</span> <span class="o">-&gt;</span> <span class="n">process</span><span class="p">(</span><span class="ss">:help</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Comparado com a versão anterior eu só adicionei o argumento string &ldquo;<tt>:source</tt>&rdquo; no OptionParser e passei o valor capturado para <tt>process/4</tt>. Eu deveria adicionar alguma validação aqui para evitar strings diferentes de &ldquo;mangareader&rdquo; ou &ldquo;mangafox&rdquo;, mas vou deixar isso pra outro momento.</p>
<p>E no módulo Workflow, em vez de começar só com a URL do mangá, agora preciso começar com a URL e a fonte do mangá:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">[</span><span class="n">url</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O que significa que cada uma dessas funções precisa não só retornar a nova lista de URLs como também passar a source adiante:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">chapters</span><span class="p">(</span><span class="o">[</span><span class="n">url</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">_manga_title</span><span class="p">,</span> <span class="n">chapter_list</span><span class="p">}</span> <span class="o">=</span> <span class="n">source</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Worker</span><span class="o">.</span><span class="n">manga_source</span><span class="p">(</span><span class="s2">&#34;IndexPage&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">apply</span><span class="p">(</span><span class="ss">:chapters</span><span class="p">,</span> <span class="o">[</span><span class="n">url</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">[</span><span class="n">chapter_list</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Essa era a única função do módulo Workflow hardcoded pro MangaReader, então eu também a deixei dinâmica usando a mesma função <tt>manga_source/2</tt> do Worker, e repare que o valor de retorno é &ldquo;<tt>[chapter_list, source]</tt>&rdquo; em vez de só &ldquo;<tt>chapter_list</tt>&rdquo;.</p>
<p>E agora eu finalmente posso testar com &ldquo;<tt>mix test</tt>&rdquo; e criar o novo binário executável de linha de comando com &ldquo;<tt>mix escript.build</tt>&rdquo; e rodar a nova versão assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>./ex_manga_downloadr -n onepunch -u http://mangafox.me/manga/onepunch_man/ -d /tmp/onepunch -s mangafox</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O site do Mangafox é bem instável com várias conexões concorrentes e dá timeout rapidinho às vezes, cuspindo erros feios assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>15:58:46.637 [error] Task #PID&lt;0.2367.0&gt; started from #PID&lt;0.124.0&gt; terminating
** (stop) exited in: GenServer.call(#PID&lt;0.90.0&gt;, {:page_download_image, {&#34;http://z.mfcdn.net/store/manga/11362/TBD-053.2/compressed/h006.jpg&#34;, &#34;Onepunch-Man 53.2: 53rd Punch [Fighting Spirit] (2) at MangaFox.me-h006.jpg&#34;}, &#34;/tmp/onepunch&#34;}, 1000000)
    ** (EXIT) an exception was raised:
        ** (HTTPotion.HTTPError) connection_closing
            (httpotion) lib/httpotion.ex:209: HTTPotion.handle_response/1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ainda não descobri como fazer retry de requisições HTTPotion direito. Mas uma coisinha que eu fiz foi adicionar um check de disponibilidade no módulo Worker. Assim você pode simplesmente rodar o mesmo comando de novo e ele vai retomar baixando só os arquivos que faltam:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">download_image</span><span class="p">({</span><span class="n">image_src</span><span class="p">,</span> <span class="n">image_filename</span><span class="p">},</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">filename</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">directory</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">image_filename</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Image </span><span class="si">#{</span><span class="n">filename</span><span class="si">}</span><span class="s2"> already downloaded, skipping.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">image_src</span><span class="p">,</span> <span class="n">filename</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Downloading image </span><span class="si">#{</span><span class="n">image_src</span><span class="si">}</span><span class="s2"> to </span><span class="si">#{</span><span class="n">filename</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">image_src</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="o">[</span><span class="ss">headers</span><span class="p">:</span> <span class="o">[</span><span class="s2">&#34;User-Agent&#34;</span><span class="p">:</span> <span class="vi">@user_agent</span><span class="o">]</span><span class="p">,</span> <span class="ss">timeout</span><span class="p">:</span> <span class="vi">@http_timeout</span><span class="o">]</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">_headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="no">File</span><span class="o">.</span><span class="n">write!</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">image_src</span><span class="p">,</span> <span class="n">filename</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:err</span><span class="p">,</span> <span class="n">image_src</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso pelo menos reduz retrabalho. Outra coisa que ainda estou trabalhando é nesse outro pedaço da função principal &ldquo;CLI.process&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">source</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">File</span><span class="o">.</span><span class="n">mkdir_p!</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">dump_file</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">directory</span><span class="si">}</span><span class="s2">/images_list.dump&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">images_list</span> <span class="o">=</span> <span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="n">dump_file</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">                  <span class="ss">:erlang</span><span class="o">.</span><span class="n">binary_to_term</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">dump_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                <span class="k">else</span>
</span></span><span class="line"><span class="cl">                  <span class="n">list</span> <span class="o">=</span> <span class="o">[</span><span class="n">url</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">                    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">                    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">                    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span>
</span></span><span class="line"><span class="cl">                  <span class="no">File</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">dump_file</span><span class="p">,</span> <span class="ss">:erlang</span><span class="o">.</span><span class="n">term_to_binary</span><span class="p">(</span><span class="n">list</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                  <span class="n">list</span>
</span></span><span class="line"><span class="cl">                <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">images_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">process_downloads</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">optimize_images</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">compile_pdfs</span><span class="p">(</span><span class="n">manga_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">finish_process</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Como dá pra ver, a ideia é serializar os links finais das URLs das imagens num arquivo usando o serializador embutido &ldquo;<tt>:erlang.binary_to_term/1</tt>&rdquo; e checar se esse arquivo de dump existe, e desserializar com &ldquo;<tt>:erlang.term_to_binary/1</tt>&rdquo; antes de buscar todas as páginas de novo. Agora o processo pode retomar direto da função <tt>process_downloads/2</tt>.</p>
<p>O Mangafox é terrivelmente instável e eu vou precisar descobrir uma forma melhor de fazer retry de conexões que deram timeout, sem precisar quebrar e reiniciar manualmente da linha de comando. Ou é um site ruim ou um site esperto que derruba scrappers como o meu, embora eu chute que seja só infraestrutura ruim do lado deles.</p>
<p>Se eu reduzo de 50 processos para 5 no pool, parece que ele consegue lidar melhor (mas o processo fica mais lento, claro):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">    <span class="n">pool_options</span> <span class="o">=</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">      <span class="nb">name</span><span class="p">:</span> <span class="p">{</span><span class="ss">:local</span><span class="p">,</span> <span class="ss">:worker_pool</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="ss">worker_module</span><span class="p">:</span> <span class="no">PoolManagement</span><span class="o">.</span><span class="n">Worker</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">size</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">max_overflow</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="o">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você ver erros de timeout, mude esse parâmetro. O MangaReader ainda aguenta 50 ou mais de concorrência.</p>
<p>E agora você sabe como adicionar suporte pra mais fontes de mangá. Fique à vontade pra mandar um Pull Request! :-)</p>
]]></content:encoded><category>elixir-e-erlang</category><category>anime-e-manga</category><category>automacao</category></item><item><title>Ex Manga Downloadr, um exercício com Elixir</title><link>https://akitaonrails.github.io/2015/11/18/ex-manga-downloadr-um-exercicio-com-elixir/</link><guid isPermaLink="true">https://akitaonrails.github.io/2015/11/18/ex-manga-downloadr-um-exercicio-com-elixir/</guid><pubDate>Wed, 18 Nov 2015 17:26:00 -0200</pubDate><description>&lt;p&gt;&lt;strong&gt;Update 19/11/15:&lt;/strong&gt; Neste artigo eu menciono algumas dúvidas que tive, então leia este e depois acompanhe na &lt;a href="http://www.akitaonrails.com/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener"&gt;Parte 2&lt;/a&gt; para ver como resolvi.&lt;/p&gt;
&lt;p&gt;Como exercício (e também porque, obviamente, sou um &lt;a href="https://en.wikipedia.org/wiki/Otaku"target="_blank" rel="noopener"&gt;Otaku&lt;/a&gt;) eu &lt;a href="https://github.com/akitaonrails/ex_manga_downloadr/blob/master/lib/ex_manga_downloadr/mangareader/index_page.ex"target="_blank" rel="noopener"&gt;implementei&lt;/a&gt; um scrapper simples em Elixir para o ótimo site &lt;a href="http://www.mangareader.net/"target="_blank" rel="noopener"&gt;MangaReader&lt;/a&gt;. Dá pra discutir se é certo fazer scrap do site deles, e dá pra discutir também se eles oferecerem esses mangás é certo em primeiro lugar, então vamos deixar essa discussão de lado.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Update 19/11/15:</strong> Neste artigo eu menciono algumas dúvidas que tive, então leia este e depois acompanhe na <a href="http://www.akitaonrails.com/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener">Parte 2</a> para ver como resolvi.</p>
<p>Como exercício (e também porque, obviamente, sou um <a href="https://en.wikipedia.org/wiki/Otaku"target="_blank" rel="noopener">Otaku</a>) eu <a href="https://github.com/akitaonrails/ex_manga_downloadr/blob/master/lib/ex_manga_downloadr/mangareader/index_page.ex"target="_blank" rel="noopener">implementei</a> um scrapper simples em Elixir para o ótimo site <a href="http://www.mangareader.net/"target="_blank" rel="noopener">MangaReader</a>. Dá pra discutir se é certo fazer scrap do site deles, e dá pra discutir também se eles oferecerem esses mangás é certo em primeiro lugar, então vamos deixar essa discussão de lado.</p>
<p>Eu tinha uma versão mais antiga <a href="https://github.com/akitaonrails/manga-downloadr"target="_blank" rel="noopener">escrita em Ruby</a>. Ela ainda funciona, mas precisa muito de uma boa refatoração (desculpem por isso). O propósito daquela versão era ver se eu conseguia fazer fetch paralelo e retry de conexões usando Typhoeus.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/525/big_ex_manga_downloadr.png" alt="OnePunch Man baixado"  loading="lazy" /></p>
<p>Conforme fui evoluindo nos meus estudos de Elixir, essa ferramenta pareceu uma ótima candidata para testar meu conhecimento atual da plataforma. Ela me obrigaria a testar:</p>
<ol>
<li>Fetch e parse de conteúdo ad hoc via HTTP (<a href="https://github.com/myfreeweb/httpotion"target="_blank" rel="noopener">HTTPotion</a> e <a href="https://github.com/philss/floki"target="_blank" rel="noopener">Floki</a>).</li>
<li>Testar downloads paralelos/assíncronos (módulo <a href="http://elixir-lang.org/docs/stable/elixir/Task.html"target="_blank" rel="noopener">Task</a> embutido no Elixir).</li>
<li>O suporte embutido para linha de comando e <a href="http://elixir-lang.org/docs/stable/elixir/OptionParser.html"target="_blank" rel="noopener">option parser</a>.</li>
<li>Testes básicos via ExUnit e mock do workflow (<a href="https://github.com/jjh42/mock"target="_blank" rel="noopener">Mock</a>).</li>
</ol>
<p>O exercício foi muito interessante, e um scrapper também é candidato ideal para TDD. Os passos iniciais tinham que ser assim:</p>
<ol>
<li>Fazer parse da página principal do mangá para pegar todos os links dos capítulos.</li>
<li>Fazer parse de cada página de capítulo para pegar todas as páginas individuais.</li>
<li>Fazer parse de cada página para extrair a imagem principal embutida (a página real do mangá).</li>
</ol>
<p>Para cada um desses passos iniciais eu fiz um teste unitário simples e os módulos <tt>IndexPage</tt>, <tt>ChapterPage</tt> e <tt>Page</tt>. Eles têm mais ou menos a mesma estrutura, este é um exemplo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">IndexPage</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">chapters</span><span class="p">(</span><span class="n">manga_root_url</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">manga_root_url</span><span class="p">,</span> <span class="o">[</span><span class="ss">timeout</span><span class="p">:</span> <span class="mi">30_000</span><span class="o">]</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">_headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">fetch_manga_title</span><span class="p">(</span><span class="n">body</span><span class="p">),</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:err</span><span class="p">,</span> <span class="s2">&#34;not found&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">fetch_manga_title</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#mangaproperties h1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;h1&#34;</span><span class="p">,</span> <span class="o">[]</span><span class="p">,</span> <span class="o">[</span><span class="n">title</span><span class="o">]</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">title</span> <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#listing a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span> <span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="o">[</span><span class="p">{</span><span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="n">url</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">url</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aqui eu já estou exercitando algumas das features mais legais do Elixir, como pattern matching no resultado da função <tt>HTTPotion.get/2</tt> para extrair o body do record que retorna.</p>
<p>Depois eu passo o body do HTML para 2 funções diferentes: <tt>fetch_manga_title</tt> e <tt>fetch_chapters</tt>. As duas usam o pacote Floki, que aceita seletores CSS para retornar uma List. Aí eu preciso percorrer essa lista (usando <tt>Enum.map/2</tt> por exemplo) e fazer pattern match nela para extrair os valores que preciso.</p>
<p><a href="http://elixir-lang.org/getting-started/pattern-matching.html"target="_blank" rel="noopener">Pattern Matching</a> é um dos conceitos mais importantes para aprender em Elixir/Erlang. É diferente de simplesmente atribuir um valor a uma variável: dá pra usar para desmontar uma estrutura nos seus componentes e pegar as partes individuais.</p>
<p>Daí eu fui montar o esqueleto da interface de linha de comando. Isso já é explicado em outros tutoriais como <a href="http://asquera.de/blog/2015-04-10/writing-a-commandline-app-in-elixir/"target="_blank" rel="noopener">este</a> e <a href="https://speakerdeck.com/st23am/writing-command-line-applications-in-elixir"target="_blank" rel="noopener">este</a>, então não vou perder tempo explicando de novo. No núcleo eu precisava do seguinte workflow:</p>
<ol>
<li>Partindo da URL principal do mangá no MangaReader, extrair os capítulos</li>
<li>Depois loopar pelos capítulos e pegar todas as páginas</li>
<li>Depois loopar pelas páginas e pegar todos os sources de imagem</li>
<li>Depois loopar pelas imagens e baixar todas para um diretório temporário</li>
<li>Depois ordenar os arquivos no diretório e movê-los para subdiretórios de 250 imagens cada (acho um bom tamanho para cada volume)</li>
<li>Por fim, redimensionar e converter todas as imagens de cada subdiretório em um arquivo PDF para o meu Kindle consumir.</li>
</ol>
<p>Esse workflow é definido assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">File</span><span class="o">.</span><span class="n">mkdir_p!</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">url</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">process_downloads</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">optimize_images</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">compile_pdfs</span><span class="p">(</span><span class="n">manga_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">finish_process</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse é um lugar onde a notação de pipeline do Elixir realmente brilha. Fica muito melhor do que ter que escrever este equivalente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Workflow</span><span class="o">.</span><span class="n">compile_pdfs</span><span class="p">(</span><span class="no">Workflow</span><span class="o">.</span><span class="n">optimize_images</span><span class="p">(</span><span class="n">directory</span><span class="p">))</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Essa notação é só açúcar sintático onde o valor de retorno do statement anterior é usado como primeiro argumento da função seguinte. Combine isso com outros açúcares sintáticos como parênteses opcionais (assim como o amado Ruby) e você tem uma exposição clara de &ldquo;transformar uma URL em PDFs compilados&rdquo;.</p>
<p>Separei o Workflow no seu próprio módulo e cada passo é bem parecido, cada um pegando uma lista e percorrendo ela. Este é o mais simples deles:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">pages</span><span class="p">(</span><span class="n">chapter_list</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">chapter_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span> <span class="no">ChapterPage</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">)</span> <span class="k">end</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">await</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="vi">@http_timeout</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p">(</span><span class="o">[]</span><span class="p">,</span> <span class="n">fn</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">list</span><span class="p">},</span> <span class="n">acc</span> <span class="o">-&gt;</span> <span class="n">acc</span> <span class="o">++</span> <span class="n">list</span> <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você é novo em Elixir, aqui vai encontrar outra esquisitice, esse <tt>&quot;&amp;(x(&amp;1))&quot;</tt>, que é só um macro de atalho para este outro statement equivalente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">fn</span> <span class="p">(</span><span class="n">list</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="no">ChapterPage</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="n">list</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><a href="http://elixir-lang.org/docs/stable/elixir/Enum.html"target="_blank" rel="noopener">Enum</a> é um dos módulos mais úteis que você precisa dominar. Se você vem de Ruby vai se sentir em casa, e tem que aprender todas as funções dele. Você normalmente vai estar transformando uma coleção em outra, então é importante estudar a fundo.</p>
<h3>Alguns problemas para entender o processamento HTTP paralelo (W.I.P.)<span class="hx:absolute hx:-mt-20" id="alguns-problemas-para-entender-o-processamento-http-paralelo-wip"></span>
    <a href="#alguns-problemas-para-entender-o-processamento-http-paralelo-wip" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aí tem esse lance de <tt>Task.async/await</tt>. Se você vem de uma linguagem que tem Threads, é bem parecido: você inicia várias Threads diferentes e espera todas retornarem antes de continuar. Mas uma Task em Elixir não é uma thread real, é uma <a href="https://en.wikipedia.org/wiki/Green_threads#Green_threads_in_other_languages"target="_blank" rel="noopener">&ldquo;green thread&rdquo;</a> ou, no jargão de Erlang, um &ldquo;process&rdquo; bem leve. Erlang usa processes para tudo, e Elixir também. Por baixo dos panos, o módulo &ldquo;Task&rdquo; encapsula todo o framework OTP de supervisors/workers. Mas em vez de ter que lidar agora com <a href="http://elixir-lang.org/getting-started/mix-otp/genserver.html"target="_blank" rel="noopener">OTP GenServer</a> decidi ir pelo caminho mais simples por enquanto, e o módulo &ldquo;Task&rdquo; cumpre essa função.</p>
<p>Daí, acabei caindo num problema. Se eu continuasse assim, disparando centenas de chamadas HTTP assíncronas, rapidamente caía nessa exception:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>17:10:55.882 [error] Task #PID&lt;0.2217.0&gt; started from #PID&lt;0.69.0&gt; terminating
** (HTTPotion.HTTPError) req_timedout
    (httpotion) lib/httpotion.ex:209: HTTPotion.handle_response/1
    (ex_manga_downloadr) lib/ex_manga_downloadr/mangareader/page.ex:6: ExMangaDownloadr.MangaReader.Page.image/1
    (elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2
    (elixir) lib/task/supervised.ex:19: Task.Supervised.async/3
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Function: #Function&lt;12.106612505/0 in ExMangaDownloadr.Workflow.images_sources/1&gt;
    Args: []</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por isso existe o <tt>@maximum_fetches 80</tt> no topo do módulo <tt>Workflow</tt>, junto com essa outra construção esquisita:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">images_sources</span><span class="p">(</span><span class="n">pages_list</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pages_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">chunk</span><span class="p">(</span><span class="vi">@maximum_fetches</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p">(</span><span class="o">[]</span><span class="p">,</span> <span class="n">fn</span> <span class="n">pages_chunk</span><span class="p">,</span> <span class="n">acc</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="n">result</span> <span class="o">=</span> <span class="n">pages_chunk</span>
</span></span><span class="line"><span class="cl">        <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span> <span class="no">Page</span><span class="o">.</span><span class="n">image</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">)</span> <span class="k">end</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">        <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">await</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="vi">@http_timeout</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">        <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">fn</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">image</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">image</span> <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">acc</span> <span class="o">++</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso pega uma lista enorme (como todas as páginas de um mangá bem longo tipo Naruto), quebra em listas menores de 80 elementos e aí dispara as Tasks assíncronas, reduzindo os resultados de volta para uma List simples. A função privada <tt>chunk/2</tt> só pega o menor tamanho entre o comprimento da lista e o valor máximo de fetches.</p>
<p>Às vezes quebra se o máximo é maior, às vezes não, então meu chute é que meu código não está lidando com instabilidades de rede (com alguma lógica de retry) ou então o site MangaReader está enfileirando acima do meu timeout designado (que setei para 30 segundos). De qualquer forma, manter o valor máximo abaixo de 100 parece ser um bom equilíbrio sem derrubar o workflow.</p>
<p>Essa é uma parte que ainda não tenho certeza do que fazer para lidar com incertezas no site externo não respondendo ou a rede caindo por um tempinho. HTTPotion tem suporte para chamadas assíncronas, mas eu não sei qual a diferença entre usar isso ou só fazer chamadas síncronas dentro de processes paralelos com Task, do jeito que estou fazendo. E em qualquer dos casos, eles são supervised workers, como lidar com as exceptions, como implementar lógica para tentar de novo quando falha? Se alguém tem mais conhecimento sobre isso, um comentário aí embaixo seria bem apreciado.</p>
<p>Por fim, tem um truque sujo por trás do motivo pelo qual eu gosto de usar o MangaReader: ele é bem amigável a scrappers porque em cada página do mangá a imagem é anotada com um atributo &ldquo;alt&rdquo; no formato &ldquo;[nome do mangá] [número do capítulo] - [número da página]&rdquo;. Então só tive que reformatar um pouco, adicionando um pad de zeros antes do número do capítulo e da página para que um sort simples dos arquivos baixados me dê a ordem correta. MangaFox não é tão amigável. Assim que reformato:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">normalize_metadata</span><span class="p">(</span><span class="n">image_src</span><span class="p">,</span> <span class="n">image_alt</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">extension</span>      <span class="o">=</span> <span class="nb">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">image_src</span><span class="p">,</span> <span class="s2">&#34;.&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">list</span>           <span class="o">=</span> <span class="nb">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">image_alt</span><span class="p">)</span>      <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reverse</span>
</span></span><span class="line"><span class="cl">  <span class="n">title_name</span>     <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">slice</span><span class="p">(</span><span class="n">list</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="no">Enum</span><span class="o">.</span><span class="n">count</span><span class="p">(</span><span class="n">list</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chapter_number</span> <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">list</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="sc">?0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">page_number</span>    <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">list</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="sc">?0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="n">image_src</span><span class="p">,</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">title_name</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">chapter_number</span><span class="si">}</span><span class="s2"> - Page </span><span class="si">#{</span><span class="n">page_number</span><span class="si">}</span><span class="s2">.</span><span class="si">#{</span><span class="n">extension</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Uma vez que tenho todas as imagens, disparo outro processo externo usando <a href="https://github.com/alco/porcelain"target="_blank" rel="noopener">Porcelain</a> para fazer shell out e rodar as ferramentas Mogrify e Convert do ImageMagick para redimensionar todas as imagens para 600x800 pixels (resolução do Kindle Voyage) e empacotá-las num arquivo PDF. O resultado são arquivos PDF com 250 páginas e cerca de 20Mb. Agora é só copiar os arquivos para o meu Kindle via USB.</p>
<p>O código do ImageMagick é bem chato, eu só gero os comandos no seguinte formato para o Mogrify:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&#34;mogrify -resize #{@image_dimensions} #{directory}/*.jpg&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E compilo os PDFs com este outro comando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&#34;convert #{volume_directory}/*.jpg #{volume_file}&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(Aliás, repare na interpolação de strings ao estilo Ruby a que estamos acostumados.)</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/526/big_11249911_10153742264949837_7817568172948440418_n.jpg" alt="OnePunch Man no Kindle"  loading="lazy" /></p>
<p>Tecnicamente eu poderia copiar os arquivos do módulo MangaReader para um novo módulo MangaFox e reaproveitar a mesma lógica de Workflow uma vez que tweaksse os parsers para lidar com o formato de página do MangaFox. Mas deixo isso como exercício para o leitor.</p>
<p>Os testes do módulo MangaReader fazem chamadas reais ao site deles. Deixei assim de propósito porque se o teste falhar significa que mudaram o formato do site e o parser precisa de ajuste para se adaptar. Mas depois de alguns anos eu nunca vi eles mudarem o suficiente para quebrar o meu velho parser em Ruby.</p>
<p>Só como exercício final eu importei o pacote Mock, para controlar como algumas peças internas da implementação do Workflow retornam. Chama-se Mock mas é mais como stub de funções específicas de um módulo. Posso declarar um bloco onde sobrescrevo o <tt>File.rename/1</tt> para que ele realmente não tente mover um arquivo que não existe no ambiente de testes. Isso deixa o teste mais frágil porque depende de uma implementação específica, o que nunca é bom, mas quando estamos lidando com I/O externo, pode ser a única opção para isolar. Foi assim que o teste do Workflow foi feito. De novo, se houver um jeito melhor estou ansioso para aprender, por favor comente aí embaixo.</p>
<p>Assim fica um teste unitário com Mock, fazendo stub dos módulos HTTPotion e File:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">test</span> <span class="s2">&#34;workflow tries to download the images&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">with_mock</span> <span class="no">HTTPotion</span><span class="p">,</span> <span class="o">[</span><span class="ss">get</span><span class="p">:</span> <span class="n">fn</span><span class="p">(</span><span class="n">_url</span><span class="p">,</span> <span class="n">_options</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="k">end</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">with_mock</span> <span class="no">File</span><span class="p">,</span> <span class="o">[</span><span class="n">write!</span><span class="p">:</span> <span class="n">fn</span><span class="p">(</span><span class="n">_filename</span><span class="p">,</span> <span class="n">_body</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kp">nil</span> <span class="k">end</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">assert</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">process_downloads</span><span class="p">(</span><span class="o">[</span><span class="p">{</span><span class="s2">&#34;http://src_foo&#34;</span><span class="p">,</span> <span class="s2">&#34;filename_foo&#34;</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="s2">&#34;/tmp&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="o">[</span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="s2">&#34;http://src_foo&#34;</span><span class="p">,</span> <span class="s2">&#34;/tmp/filename_foo&#34;</span><span class="p">}</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">      <span class="n">assert</span> <span class="n">called</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;http://src_foo&#34;</span><span class="p">,</span> <span class="o">[</span><span class="ss">timeout</span><span class="p">:</span> <span class="mi">30_000</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">assert</span> <span class="n">called</span> <span class="no">File</span><span class="o">.</span><span class="n">write!</span><span class="p">(</span><span class="s2">&#34;/tmp/filename_foo&#34;</span><span class="p">,</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Tem sido uma experiência muito divertida, ainda que muito curta, e boa o suficiente para colocar em prática o que aprendi até agora. Código como este me faz sorrir:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">[</span><span class="n">destination_file</span><span class="o">|</span><span class="n">_rest</span><span class="o">]</span> <span class="o">=</span> <span class="nb">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="s2">&#34;/&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reverse</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A forma como posso fazer pattern match para extrair o head de uma lista é uma maneira diferente de pensar. Daí tem a outra forma de pensar mais importante: tudo é uma cadeia de transformação, uma aplicação é uma forma de partir de algum argumento de entrada (como uma URL) e ir passo a passo &ldquo;transformando&rdquo; ele em uma coleção de arquivos PDF, por exemplo.</p>
<p>Em vez de pensar em como arquitetar classes e objetos, começamos a pensar em quais são os argumentos iniciais e qual o resultado que quero atingir, e vamos daí, uma pequena função de transformação por vez.</p>
<p>O módulo Workflow é um exemplo. Eu na verdade comecei escrevendo tudo numa única função grande no módulo CLI. Aí refatorei em funções menores e encadeei elas para criar o Workflow. Por fim, só movi todas as funções para o módulo Workflow e chamei a partir do módulo CLI.</p>
<p>Por causa da ausência de estado global, pensar em funções pequenas e isoladas, tanto refatorar quanto fazer test-driven development ficam muito mais suaves do que em linguagens OOP. Esse jeito de pensar é, admito, lento de pegar o jeito, mas depois começa a parecer muito natural e rapidamente direciona a sua forma de programar para um código mais enxuto.</p>
<p>E os aspectos dinâmicos tanto de Erlang quanto de Elixir me fazem sentir em casa, como se fosse um &ldquo;Ruby melhorado&rdquo;.</p>
<p>O código do downloader está todo no <a href="https://github.com/akitaonrails/ex_manga_downloadr/blob/master/lib/ex_manga_downloadr/mangareader/index_page.ex"target="_blank" rel="noopener">Github, podem fazer fork</a>.</p>
<p>Estou ansioso para exercitar mais. Espero que isso motive você a aprender Elixir. E se você já é um programador avançado em Elixir ou Erlang, não esqueça de comentar aí embaixo e até mandar um Pull Request para melhorar este pequeno exercício. Ainda sou iniciante e tem muito espaço para aprender mais. Todas as contribuições são muito apreciadas.</p>
]]></content:encoded><category>elixir-e-erlang</category><category>anime-e-manga</category><category>automacao</category></item><item><title>[Small Bites] Downloader para MangaReader (Kindle Edition)</title><link>https://akitaonrails.github.io/2014/12/17/small-bites-downloader-para-mangareader-kindle-edition/</link><guid isPermaLink="true">https://akitaonrails.github.io/2014/12/17/small-bites-downloader-para-mangareader-kindle-edition/</guid><pubDate>Wed, 17 Dec 2014 21:24:00 -0200</pubDate><description>&lt;p&gt;Acho que esta vai ser realmente a primeira small bite versão miniatura mesmo! :-)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/476/IMG_20141216_162841251.jpg" alt="Kindle Paperwhite &amp;#43; Manga = Diversão" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Quem acompanha o blog faz tempo sabe que curto muito mangas/animes (me perguntem qualquer coisa, especialmente se for das décadas de 60 a 90). 4 anos atrás eu fiz um &lt;a href="http://www.akitaonrails.com/2010/07/31/downloader-para-onemanga-com#.VJIOdcZHnxg"target="_blank" rel="noopener"&gt;downloader&lt;/a&gt; para o antigo site OneManga (que fechou).&lt;/p&gt;
&lt;p&gt;Recentemente resolvi adquirir um Kindle Paperwhite (que aliás, é excepcional, principalmente se comparado ao último Kindle 2 que eu tive). E além de tudo é o formato perfeito para mangas (que são preto-e-branco por natureza).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Acho que esta vai ser realmente a primeira small bite versão miniatura mesmo! :-)</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/476/IMG_20141216_162841251.jpg" alt="Kindle Paperwhite &#43; Manga = Diversão"  loading="lazy" /></p>
<p>Quem acompanha o blog faz tempo sabe que curto muito mangas/animes (me perguntem qualquer coisa, especialmente se for das décadas de 60 a 90). 4 anos atrás eu fiz um <a href="http://www.akitaonrails.com/2010/07/31/downloader-para-onemanga-com#.VJIOdcZHnxg"target="_blank" rel="noopener">downloader</a> para o antigo site OneManga (que fechou).</p>
<p>Recentemente resolvi adquirir um Kindle Paperwhite (que aliás, é excepcional, principalmente se comparado ao último Kindle 2 que eu tive). E além de tudo é o formato perfeito para mangas (que são preto-e-branco por natureza).</p>
<p>Procurei ferramentas e achei vários horríveis, pra Windows. Então, por que não desenferrujar um pouco e fazer um pacote completo que baixa sua coleção completa de mangas e já gera um PDF otimizado pro tamanho do Kindle?</p>
<p>Aproveitei pra abusar um pouco do <a href="https://github.com/typhoeus/typhoeus"target="_blank" rel="noopener">Typhoeus</a> que usa a libcurl por baixo e permite fazer downloads paralelos. A única coisa que vai limitar o processo é a velocidade de banda do seu provedor de internet.</p>
<p>Sem enrolar mais, o código-fonte <a href="https://github.com/akitaonrails/manga-downloadr"target="_blank" rel="noopener">está no Github</a> ou instale diretamente a <a href="https://rubygems.org/gems/manga-downloadr"target="_blank" rel="noopener">gem</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gem install manga-downloadr</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E para usar, navegue no site do <a href="http://www.mangareader.net"target="_blank" rel="noopener">MangaReader.net</a> escolha o manga que quiser e faça assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>manga-downloadr -n monster -u http://www.mangareader.net/99/monster.html -d /Users/akitaonrails/Documents/MangaReader</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E pronto! Se acontecer alguma interrupção (a internet cair ou coisa parecida) só execute novamente o mesmo comando e ele vai continuar de onde parou. Boa diversão!</p>
<p>PS: Não preciso dizer que não funciona em Windows ;-)</p>
]]></content:encoded><category>anime-e-manga</category><category>ruby</category><category>automacao</category></item><item><title>[Small Bites] Vagrant + Ubuntu 14.04 + Chef Solo</title><link>https://akitaonrails.github.io/2014/07/28/small-bites-vagrant-ubuntu-14-04-chef-solo/</link><guid isPermaLink="true">https://akitaonrails.github.io/2014/07/28/small-bites-vagrant-ubuntu-14-04-chef-solo/</guid><pubDate>Mon, 28 Jul 2014 20:22:00 -0300</pubDate><description>&lt;p&gt;Este post é dedicado a quem utilizar Mac OS X (ou mesmo Windows) como sistema de desenvolvimento. Para quem usa uma distro Linux, adapte para usar o &lt;a href="https://github.com/fgrehm/vagrant-lxc"target="_blank" rel="noopener"&gt;plugin de LXC&lt;/a&gt; do Vagrant.&lt;/p&gt;
&lt;p&gt;A versão TL;DR é muito simples. Baixe e instale o &lt;a href="https://www.virtualbox.org/wiki/Downloads"target="_blank" rel="noopener"&gt;VirtualBox&lt;/a&gt; e o &lt;a href="http://www.vagrantup.com/downloads.html"target="_blank" rel="noopener"&gt;Vagrant&lt;/a&gt;. Assumindo que está no OS X (já tem Ruby 2.0.0 pré-instalado), faça o seguinte do Terminal:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;pre&gt;&lt;code&gt;sudo gem install berkshelf
vagrant plugin install vagrant-berkshelf
vagrant plugin install vagrant-omnibus
mkdir ~/Vagrant
cd ~/Vagrant
git clone https://github.com/akitaonrails/vagrant-railsbox.git railsbox
cd railsbox
export VAGRANT_SYNCED_FOLDER=~/Sites
berks install
vagrant up --provision&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Note que o &lt;tt&gt;Vagrantfile&lt;/tt&gt; vai buscar a variável de ambiente &lt;tt&gt;VAGRANT_SYNCED_FOLDER&lt;/tt&gt;, não esqueça de colocar no seu &lt;tt&gt;.zshrc&lt;/tt&gt; ou &lt;tt&gt;.bashrc&lt;/tt&gt; apontando para o diretório onde você costuma clonar seus projetos. Ele será montado, &lt;a href="https://docs.vagrantup.com/v2/synced-folders/nfs.html"target="_blank" rel="noopener"&gt;via NFS&lt;/a&gt;, dentro do box Vagrant no diretório &lt;tt&gt;/vagrant&lt;/tt&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é dedicado a quem utilizar Mac OS X (ou mesmo Windows) como sistema de desenvolvimento. Para quem usa uma distro Linux, adapte para usar o <a href="https://github.com/fgrehm/vagrant-lxc"target="_blank" rel="noopener">plugin de LXC</a> do Vagrant.</p>
<p>A versão TL;DR é muito simples. Baixe e instale o <a href="https://www.virtualbox.org/wiki/Downloads"target="_blank" rel="noopener">VirtualBox</a> e o <a href="http://www.vagrantup.com/downloads.html"target="_blank" rel="noopener">Vagrant</a>. Assumindo que está no OS X (já tem Ruby 2.0.0 pré-instalado), faça o seguinte do Terminal:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo gem install berkshelf
vagrant plugin install vagrant-berkshelf
vagrant plugin install vagrant-omnibus

mkdir ~/Vagrant
cd ~/Vagrant
git clone https://github.com/akitaonrails/vagrant-railsbox.git railsbox
cd railsbox

export VAGRANT_SYNCED_FOLDER=~/Sites

berks install
vagrant up --provision</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Note que o <tt>Vagrantfile</tt> vai buscar a variável de ambiente <tt>VAGRANT_SYNCED_FOLDER</tt>, não esqueça de colocar no seu <tt>.zshrc</tt> ou <tt>.bashrc</tt> apontando para o diretório onde você costuma clonar seus projetos. Ele será montado, <a href="https://docs.vagrantup.com/v2/synced-folders/nfs.html"target="_blank" rel="noopener">via NFS</a>, dentro do box Vagrant no diretório <tt>/vagrant</tt>.</p>
<p>Pronto, isso deve ser suficiente. Ao final você terá um ambiente de desenvolvimento com os seguintes componentes principais:</p>
<ul>
<li>RVM (receita &lsquo;user_install&rsquo;, onde você mesmo deve instalar os rubies que precisar manualmente)</li>
<li>Oh-My-Zsh (isso não seria necessário, mas ZSH é tão bom que é melhor já ter pré-instalado)</li>
<li>MySQL</li>
<li>Postgresql</li>
<li>MongoDB</li>
<li>Redis 2</li>
<li>Elasticsearch</li>
<li>Node.js, Java 1.7, Git, Imagemagick</li>
</ul>
<p>Acesse seu novo box com <tt>vagrant ssh</tt>, instale seu primeiro Ruby com <tt>rvm install ruby-2.1.2</tt> e pronto!</p>
<h2>Alguns Conceitos de Vagrant e Chef<span class="hx:absolute hx:-mt-20" id="alguns-conceitos-de-vagrant-e-chef"></span>
    <a href="#alguns-conceitos-de-vagrant-e-chef" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Meu projetinho <a href="https://github.com/akitaonrails/vagrant-railsbox"target="_blank" rel="noopener">vagrant-railsbox</a> por si só é estruturado como uma receita Chef. Ele tem um <tt>metadata.rb</tt> que funciona mais ou menos como uma &ldquo;gemspec&rdquo;, se fosse uma Rubygem.</p>
<p>Para gerenciar as dependências, uso o <a href="http://berkshelf.com/"target="_blank" rel="noopener">Berkshelf</a>, que funciona como se fosse um Bundler. Ao rodar <tt>berks install</tt> ele vai usar o arquivo <tt>Berksfile.lock</tt> (novamente, similar ao &ldquo;Gemfile.lock&rdquo; dos seus projetos Ruby). O arquivo <tt>Berksfile</tt> tem as definições de versão ou fonte, e no caso desta receita, puxa as dependências do <tt>metadata.rb</tt>. Leia mais sobre como usar <a href="http://misheska.com/blog/2013/06/16/getting-started-writing-chef-cookbooks-the-berkshelf-way/"target="_blank" rel="noopener">Berkshelf com Vagrant neste post</a>.</p>
<p>O arquivo <tt>Vagrantfile</tt> tem as configurações da receita. Na teoria eu deveria colocar as configurações que você encontra no bloco <tt>config.vm.provision :chef_solo</tt> no diretório <tt>attributes</tt>, que toda receita Chef tem, mas neste caso, como é muito específico ao Vagrant, deixei lá mesmo.</p>
<p>Note que estou usando um IP interno fixo com <tt>config.vm.network :private_network, ip: &ldquo;192.168.50.4&rdquo;</tt> em vez de usar a configuração com DHCP que seria <tt>, type: &lsquo;dhcp&rsquo;</tt>, porque acho que no VMWare Fusion (que eu uso) estava dando problema. Tente usar DHCP e veja se no seu caso funciona, senão mantenha fixo.</p>
<p>Como é máquina de desenvolvimento, a senha do usuário &lsquo;root&rsquo; do MySQL é simplesmente &lsquo;root&rsquo;. E a senha do usuário &lsquo;postgres&rsquo; é simplesmente &lsquo;postgres&rsquo;, além de estar tudo como &rsquo;trust&rsquo; no arquivo <tt>pg_hba.conf</tt>. Obviamente, estas configurações não devem ser mantidas assim se for subir em produção. Redis, Mongo e Elasticsearch, está tudo na configuração padrão e tudo sem proteção.</p>
<p>Para quem nunca usou Chef, o conceito é simples:</p>
<ul>
<li>procure por receitas no <a href="http://supermarket.getchef.com/"target="_blank" rel="noopener">Chef Supermarket</a></li>
<li>adicione no seu <tt>metadata.rb</tt> como <tt>depends &lsquo;git&rsquo;</tt></li>
<li>adicione no seu <tt>recipes/default.rb</tt> como <tt>include_recipe &lsquo;git&rsquo;</tt> ou o que a documentação da receita mandar</li>
<li>configure o <tt>node[&lsquo;git&rsquo;]</tt> no <tt>attributes/default.rb</tt> (ou, neste exemplo, no <tt>Vagrantfile</tt>), conforme a documentação</li>
</ul>
<p>A receita é onde fica os comandos para instalar e configurar o que você precisa. Atributos são os valores customizáveis que variam de instalação para instalação. No diretório <tt>files</tt> costuma ficar templates ERB para arquivos de configuração, do tipo que você editaria em <tt>/etc</tt>. Em <tt>libraries</tt> ficam classes Ruby que organizam lógicas mais complexas que você quer estruturar para reusar.</p>
<p>Resources e Providers são conceitos mais complicados de explicar, e como a idéia deste post não é detalhar Chef, vamos apenas resumir dizendo que resources definem o estado de um sistema. Por exemplo, num cookbook de NGINX teríamos o pacote de instalação, o serviço e o arquivo de configuração. Providers são as ações que levam o sistema ao estado definido, como instalar o pacote propriamente dito, editar o arquivo de configuração e configurar o serviço. Se quiser saber mais sobre o assunto, <a href="http://neethack.com/2013/10/understand-chef-lwrp-heavy-version/"target="_blank" rel="noopener">leia este post</a> e <a href="http://tech.yipit.com/2013/05/09/advanced-chef-writing-heavy-weight-resource-providers-hwrp/"target="_blank" rel="noopener">este outro</a>.</p>
<p>A grande vantagem é você poder modificar este cookbook e incrementalmente instalar coisas novas no seu box com o comando <tt>vagrant provision</tt>. E também poder simplesmente recomeçar do zero, destruindo seu box com <tt>vagrant destroy</tt> e reiniciando com <tt>vagrant up &ndash;provision</tt> e chegar na mesma configuração final. É sempre bom pode começar do zero sem dores de cabeça.</p>
<p>Sobre montar seu próprio cookbook, a parte mais complicada é buscar os cookbooks que já existem, entender como eles funcionam, e isso requer ler as receitas, ler a documentação, entender dependências como ver que o oh-my-zsh depende o data_bag do cookbook &lsquo;user&rsquo;, por exemplo. Antes de sair montando tudo do zero, procure no supermarket e, obviamente, no Google, para buscar direto no Github.</p>
<p>Alguém tem mais dicas para melhorar este cookbook? Não deixe de comentar aqui ou mesmo mandar um Pull Request pra mim!</p>
]]></content:encoded><category>devops</category><category>linux</category><category>automacao</category></item><item><title>Conversando com Adam Jacob</title><link>https://akitaonrails.github.io/2009/11/18/conversando-com-adam-jacob/</link><guid isPermaLink="true">https://akitaonrails.github.io/2009/11/18/conversando-com-adam-jacob/</guid><pubDate>Wed, 18 Nov 2009 11:46:00 -0200</pubDate><description>&lt;p&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/Opscode_logo_final_full_aspect_medium_original.png" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Gerenciamento de Configuração é um assunto complicado. Para quem não está familiarizado: quando você é um desenvolvedor com poucos servidores para cuidar, geralmente dá para se virar gerenciando-os manualmente. As pessoas provavelmente estão acostumadas a colocar um CD, dar duplo clique no programa &amp;ldquo;instalar&amp;rdquo; e clicar &amp;ldquo;avançar&amp;rdquo;, &amp;ldquo;avançar&amp;rdquo; até o fim, depois entrar manualmente para fazer backup (quando se lembra), e às vezes até aplicar atualizações de segurança quando se lembra delas.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/Opscode_logo_final_full_aspect_medium_original.png" alt=""  loading="lazy" /></p>
<p>Gerenciamento de Configuração é um assunto complicado. Para quem não está familiarizado: quando você é um desenvolvedor com poucos servidores para cuidar, geralmente dá para se virar gerenciando-os manualmente. As pessoas provavelmente estão acostumadas a colocar um CD, dar duplo clique no programa &ldquo;instalar&rdquo; e clicar &ldquo;avançar&rdquo;, &ldquo;avançar&rdquo; até o fim, depois entrar manualmente para fazer backup (quando se lembra), e às vezes até aplicar atualizações de segurança quando se lembra delas.</p>
<p>Mas quando você tem mais de uma dúzia de máquinas, as coisas começam a ficar feias. Você acaba cometendo mais erros, esquecendo etapas importantes, e de repente gerenciar máquinas vira um pesadelo. Você acaba sendo acordado no meio da noite porque esqueceu de instalar algum componente crucial, e por aí vai.</p>
<p>Da mesma forma que você precisa de testes e ferramentas de integração contínua como desenvolvedor, você também precisa de ferramentas automatizadas, confiáveis e flexíveis para o papel de administrador de sistemas. É aí que ferramentas como o <strong>Chef</strong> entram para ajudar.</p>
<p>Da <a href="http://opscode.com/"target="_blank" rel="noopener">Opscode Inc.</a>, temos <a href="http://twitter.com/adamhjk"target="_blank" rel="noopener">Adam Jacob</a> (CTO) e <a href="http://twitter.com/jesserobbins"target="_blank" rel="noopener">Jesse Robbins</a> (CEO) para falar sobre o novo concorrente no campo de administração de sistemas automatizada, o <a href="http://www.opscode.com/chef/"target="_blank" rel="noopener">Chef</a>, já em uso em muitas empresas que se esforçam na vanguarda para manter seus datacenters.</p>
<p><strong>AkitaOnRails:</strong> Para começar esta entrevista, seria ótimo ter mais informações de background sobre vocês. Como vocês chegaram ao espaço de gerenciamento de configuração?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/adam_jacob_original_original.png" alt=""  loading="lazy" /></p>
<p><strong>Adam:</strong> Sou administrador de sistemas há 13 anos e, na maior parte desse tempo, trabalhei em empresas que faziam muitas fusões e aquisições. A cada dois meses adquiríamos uma nova empresa e era minha função ajudar a descobrir como absorver essas empresas no todo. Fiquei muito bom em analisar uma aplicação com a qual não tinha nada a ver com a criação e descobrir o que precisava ser feito para escalá-la (ou pelo menos fazê-la funcionar).</p>
<p>Uma coisa que não podíamos fazer era dizer às pessoas que construíram as aplicações que elas precisavam ser radicalmente alteradas — em muitos casos, elas nem estavam mais lá. O que isso significava na prática era que precisávamos de uma arquitetura subjacente muito flexível e modular — tudo que fazíamos tinha que servir à aplicação, e não o contrário. Por necessidade, isso significou me tornar um desenvolvedor de ferramentas — se não tivéssemos as ferramentas de que precisávamos, nunca conseguiríamos fazer o trabalho diante de nós.</p>
<p>Eventualmente cofundei uma empresa de consultoria chamada HJK Solutions. Construímos infraestrutura totalmente automatizada para startups — desde a instalação do SO até o deployment de aplicações, tudo totalmente automatizado (incluindo Gerenciamento de Identidade, Monitoramento e Tendências, etc.). Ao longo dos dois anos seguintes, construímos infraestrutura para 12 startups diferentes, cujos produtos iam de gerenciamento de frotas de carros elétricos a namoro online.</p>
<p>Então chego ao gerenciamento de configuração pelas trincheiras — como administrador de sistemas de linha tentando facilitar minha vida, como consultor ajudando outros a colher os benefícios, e agora como construtor de ferramentas tentando avançar o estado da arte.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/Jesse_Robbins_original_original.png" alt=""  loading="lazy" /></p>
<p><strong>Jesse:</strong> Jesse Robbins é CEO da Opscode (criadores do Chef) e um especialista reconhecido em Infraestrutura, Operações Web e Gerenciamento de Emergências. Atua como co-presidente da Velocity Web Performance &amp; Operations Conference e contribui para o O&rsquo;Reilly Radar. Antes de cofundar a Opscode, trabalhou na Amazon.com com o título de &ldquo;Master of Disaster&rdquo; onde era responsável pela disponibilidade do site para todas as propriedades com a marca Amazon. Jesse é bombeiro voluntário/técnico de emergências médicas e gerente de emergências, e liderou uma força-tarefa implantada na Operação Furacão Katrina. Suas experiências no serviço de bombeiros influenciam profundamente seus esforços em tecnologia, e ele se esforça para destilar seu conhecimento desses dois mundos e aplicá-lo em serviço de ambos.</p>
<p><strong>AkitaOnRails:</strong> Qual é a história por trás da Opscode, qual é sua missão e qual é a história da criação do Chef?</p>
<p><strong>Opscode:</strong> A história por trás da Opscode realmente começa quando conhecemos Jesse. Ele havia escrito um artigo para o O&rsquo;Reilly Radar sobre <a href="http://radar.oreilly.com/2007/10/operations-is-a-competitive-ad.html"target="_blank" rel="noopener">Operações sendo o novo molho secreto</a> para startups. Ele e eu tomamos um café, nos tornamos amigos e ficamos em contato. Jesse entende a cultura de operações de forma visceral, e está muito bem conectado a uma enorme comunidade de pessoas com mentalidade semelhante que eu nem sabia que existia.</p>
<p>À medida que nossa empresa de consultoria crescia, chegamos a uma encruzilhada — claramente tínhamos um bom negócio, e não era difícil encontrar mais trabalho. O que era difícil era encontrar pessoas com as habilidades para de fato fazer o deploy de uma nova infraestrutura e adaptar a stack que desenvolvemos para uma nova aplicação. Não podíamos evitar o fato de que havia algumas semanas de trabalho muito intenso necessário para colocar toda a infraestrutura funcionando para um novo cliente.</p>
<p>Tentamos recrutar Jesse durante esse período, já que ele era um daquelas pessoas raras que podiam fazer esse engajamento inicial intenso. Ele recusou — principalmente porque via o que fazíamos: poderíamos construir uma empresa de consultoria enorme, mas ainda seria nas trincheiras todos os dias. A menos que conseguíssemos superar esse obstáculo, provavelmente havia formas menos estressantes de ganhar a vida. :)</p>
<p>Então começamos a analisar o que nos impedia de conseguir essa parte inicial do engajamento o mais rapidamente possível. O que nos impedia de literalmente fazer um cliente preencher um questionário e deixar esses dados orientar 95% das decisões sobre a infraestrutura?</p>
<p>A resposta, em uma palavra, era &ldquo;tudo&rdquo;. Toda a stack de ferramentas open source que estávamos usando havia sido construída em uma era diferente e via o mundo por uma lente muito diferente. Precisávamos que tudo tivesse uma API, precisávamos que tudo fosse mais aberto com seus dados e precisávamos que tudo fosse flexível o suficiente para lidar com a próxima evolução das arquiteturas de aplicações (seja lá o que isso fosse ser).</p>
<p>Com essa revelação, o próximo passo foi descobrir como seria uma &ldquo;nova stack&rdquo; de verdade. Se pudéssemos começar do zero, o que levaríamos e o que deixaríamos para trás? A Amazon havia feito um trabalho tão bom mostrando como uma API sobre o processo de bootstrapping poderia parecer, e os benefícios que poderiam ser obtidos com essa abordagem. Supondo que algo assim existisse para bootstrapping, e a próxima camada da stack (configuração)?</p>
<p>Começamos a experimentar com o Chef durante esse período de questionamentos. Começamos a trabalhar no Chef com o objetivo de nos colocar fora dos negócios — tornando a barreira de entrada para ter uma infraestrutura totalmente automatizada tão baixa que qualquer desenvolvedor ou administrador de sistemas pudesse simplesmente fazê-lo. Construímos um protótipo, mostramos para Jesse e ele concordou em se juntar ao time.</p>
<p>A Opscode nasceu então, e nossa missão surgiu desses experimentos: estamos levando a Automação de Infraestrutura para as Massas. Queremos derrubar as barreiras de entrada que impedem as pessoas de ter uma infraestrutura realmente ótima, repetível e automatizada. Nosso papel é trazer aos desenvolvedores e administradores de sistemas as melhores ferramentas possíveis para que possam construir os sistemas que sempre quiseram.</p>
<p>Captamos 2,5 milhões da DFJ em janeiro de 2009 com essa visão e desde então temos trabalhado nisso.</p>
<p><strong>AkitaOnRails:</strong> Gostaria de dizer que apenas sysadmins amadores fazem tudo manualmente, mas acho que a maioria das corporações pequenas e médias ainda faz tudo manualmente ou com scripts aleatórios espalhados por todo lugar. A noção de &ldquo;gerenciamento de configuração&rdquo; ainda é nova para muita gente. Você poderia explicar brevemente o que é e por que é importante?</p>
<p><strong>Opscode:</strong> Para mim, &ldquo;Gerenciamento de Configuração&rdquo; em sua essência é tudo que você precisa fazer para levar um sistema de &ldquo;rodando um sistema operacional&rdquo; para &ldquo;fazendo seu trabalho&rdquo;. Quando um administrador de sistemas configura um sistema manualmente e posta suas notas em um Wiki, ele está praticando a forma mais primitiva de Gerenciamento de Configuração.</p>
<p>Ter essas notas é melhor que nada — quando ela precisar fazer aquela tarefa de novo, pode pelo menos voltar e lê-las para se lembrar do que fez da última vez. Mas ainda precisa fazer tudo de novo — e a repetição cansa. Então ela começa a escrever scripts que encapsulam esse conhecimento em código, de modo que agora só precisa executar uma série de comandos menores.</p>
<p>O tempo passa, porém, e a entropia entra em ação. Os sistemas começam a derivar de onde estavam quando o administrador escreveu os scripts. Logo os scripts não rodam mais, ou se rodam, a configuração que constroem está errada. Nossa destemida admin começa então a editar os scripts ou criar novos para lidar com o sistema nesse novo estado. Isso é o estágio que chamamos de &ldquo;tool sprawl&rdquo; — você tem uma ferramenta para cada fase diferente do ciclo de vida observável de um sistema.</p>
<p>As ferramentas modernas de gerenciamento de configuração resolvem esse problema fornecendo um framework para descrever o estado final em que um sistema deve estar, em vez dos passos discretos que devemos seguir para chegar lá em qualquer momento. Em vez de escrever um script que lista os comandos para instalar o Apache, escrever o arquivo de configuração e iniciar o serviço, você descreveria que &ldquo;apache deve estar instalado&rdquo;, o arquivo de configuração &ldquo;deve ter esta aparência&rdquo; e o serviço &ldquo;deve estar rodando&rdquo;.</p>
<p>Quando o sistema de gerenciamento de configuração roda, ele analisa cada uma dessas descrições individualmente e garante que estejam no estado adequado. Não nos importa mais qual é o estado inicial do sistema — o sistema de gerenciamento de configuração só tomará ação em cada etapa se o sistema não estiver no estado que você descreveu. Se você rodar novamente e nada tiver mudado, o sistema não toma nenhuma ação. (Os geeks de gerenciamento de configuração chamam essa propriedade de <a href="http://en.wikipedia.org/wiki/Idempotence"target="_blank" rel="noopener">idempotência</a>)</p>
<p>Isso significa que você pode descrever seus sistemas uma vez em código e, à medida que mudam com o tempo, simplesmente atualizar esse código para refletir o estado desejado. O impacto desse modelo no dia a dia de um administrador de sistemas não pode ser subestimado — torna tudo mais fácil.</p>
<p>Também tem impactos enormes fora do mundo dos administradores de sistemas. Você agora tem um documento vivo que descreve como todos os seus servidores estão configurados — pode compartilhá-lo, colocá-lo em controle de versão, imprimi-lo para um auditor. Processos de negócios que antes exigiam intervenção manual frequentemente podem ser reduzidos a mudanças discretas. Quanto mais você trabalha dessa forma, mais os impactos se espalham pela organização.</p>
<p><strong>AkitaOnRails:</strong> Embora a comunidade Ruby provavelmente compare Chef com Puppet, acho que um dos sistemas mais amplamente utilizados é o CFEngine2. Como o Chef se compara ao CFEngine2?</p>
<p><strong>Opscode:</strong> O Cfengine 2 é onde quase todo mundo no mundo do gerenciamento de configuração aprendeu. Mark Burgess é responsável pelos artigos acadêmicos que delinearam a ideia de que cada parte do sistema deve ser idempotente, e seu trabalho estudando como sistemas do mundo real podem ser gerenciados em escala fez mais para impactar a evolução do gerenciamento de configuração do que qualquer outro indivíduo.</p>
<p>Além da ideia de recursos idempotentes, Burgess introduziu o conceito maior de &ldquo;convergência&rdquo;. A ideia básica aqui é que se você tem a descrição de como cada recurso finito deve ser configurado, dado tempo suficiente, esses recursos trarão o sistema para um estado compatível. A ordem em que os recursos rodam fundamentalmente não importa — eventualmente, todos chegarão ao lugar certo.</p>
<p>Embora esse modelo funcione em um nível fundamental, tem algumas ineficiências bastante dramáticas. O Cfengine aplica recursos com base em seu tipo — todos os arquivos são gerenciados de uma vez, depois todos os serviços, depois todos os pacotes. Você pode controlar a ordem em que são executados com o &lsquo;actionsequence&rsquo;, mas cada sistema tem apenas uma ordem. Então se você precisa copiar um arquivo, instalar um pacote e reiniciar um serviço, é fácil o suficiente de modelar. Quando você começa a entrar em configurações mais complexas, porém, fica cada vez mais difícil montar um actionsequence que permite configurar todo o seu sistema em uma única execução.</p>
<p>Isso não é um bug no Cfengine — é por design. A convergência é a resposta — não tem problema precisar rodar mais de uma vez; eventualmente chegará lá. Se você está pensando demais sobre a ordem em que as coisas devem acontecer, provavelmente não está pensando em descrições idempotentes.</p>
<p>Na minha experiência, porém, isso era frustrante em escala. Significava que o tempo necessário para configurar um sistema aumentava à medida que a configuração ficava mais complicada, e sua capacidade de modelar interações complexas no sistema tornava-se cada vez mais opaca.</p>
<p>Para a era em que foi escrito, esse problema importava muito menos. Se leva de 6 a 8 semanas para pedir um servidor, depois mais uma semana para instalá-lo no rack e instalar o SO, o fato de que o Cfengine precisa rodar 3 vezes para configurá-lo não importa muito. (Você tem problemas muito maiores!) Em um mundo onde muitos de nós podemos ir de bare-metal a um sistema operacional rodando em 5 minutos, seja por nossos próprios sistemas de instalação ou por uma chamada de API à AWS, isso começa a importar muito.</p>
<p>O Chef adota uma abordagem diferente. Começamos com a ideia de um &ldquo;recurso&rdquo; idempotente — um pacote, um serviço, etc. Adicionamos a ideia de &ldquo;ações&rdquo; — todos os diferentes estados que você pode querer solicitar que um recurso esteja. Depois você coloca esses recursos em &ldquo;recipes&rdquo;, que são avaliadas na ordem em que são escritas. Você pode então fazer com que recipes dependam de outras recipes que tenham sido concluídas antes de serem executadas, dando-lhe a capacidade de dizer &ldquo;certifique-se de que o Apache está instalado antes de configurar minha Aplicação Web&rdquo;.</p>
<p>O objetivo é que, com Chef, você sempre possa trazer o sistema ao estado adequado com uma única execução do Chef. Ele vê a convergência como uma resposta a um bug — um sistema que está 1/2 configurado está, de fato, quebrado. Se a causa do bug for ambiental — um serviço de rede não está disponível, por exemplo — então rodar o Chef novamente provavelmente corrigirá as coisas. Se o problema é que você não especificou a ordem em que quer que os recursos sejam configurados, então é um bug em suas recipes. Se você não consegue escrever uma recipe para configurar seu sistema em uma única execução do Chef, é um bug no Chef.</p>
<p>Isso também tem efeitos colaterais para raciocinar sobre o sistema em escala. Dada a mesma configuração e atributos, o Chef sempre se comportará da mesma forma, em todos os sistemas. À medida que você adiciona mais e mais coisas ao sistema, ainda é fácil raciocinar sobre quando e como elas serão configuradas.</p>
<p>Curiosamente, o Cfengine 3 implementa um sistema muito similar ao Chef. Em vez de uma actionsequence global única, usa um &lsquo;bundlesequence&rsquo;, onde bundles são aproximadamente análogos ao conceito de recipe do Chef.</p>
<p>Outra diferença importante é que o Chef permite que você &lsquo;olhe para cima&rsquo; em toda a sua infraestrutura ao configurar um sistema. Essa capacidade é útil quando você quer fazer coisas como configurar um sistema de monitoramento ou um load balancer. Você pode fazer perguntas ao Chef como &ldquo;quais são todos os servidores rodando minha aplicação em produção&rdquo; e usar a resposta para configurar seu sistema. (Você pode ver um exemplo disso no <a href="http://www.opscode.com/blog/2009/10/07/preview-chef-0-8-and-the-opscode-platform/"target="_blank" rel="noopener">nosso blog</a>)</p>
<p>Do ponto de vista de arquitetura de sistemas, Chef e Cfengine são na verdade bastante similares. Ambos empurram a maior parte do trabalho difícil de configurar um sistema para os próprios sistemas. Isso é muito vantajoso em escala — os servidores do Chef e do Cfengine são na verdade sistemas glorificados de transferência de arquivos.</p>
<p>Diferimos bastante em nossa abordagem sobre como deve ser uma linguagem para gerenciamento de configuração, mas falarei mais sobre isso numa pergunta posterior.</p>
<p>O Cfengine está em uso em alguns dos maiores datacenters da indústria e alterou fundamentalmente o panorama do gerenciamento de sistemas. Embora não seja mais a ferramenta que quero, não se pode subestimar o impacto que teve no design de todas as ferramentas de gerenciamento de configuração que vieram depois — incluindo o Chef.</p>
<p><strong>AkitaOnRails:</strong> O Chef tem muitos componentes. Você pode descrever brevemente todos os principais componentes que trabalham juntos? O lado do cliente, o lado do servidor, os cookbooks?</p>
<p><strong>Opscode:</strong> Claro! A parte mais importante do Chef são os cookbooks — é onde você realmente descreve sua configuração. Eles coletam recipes e todos os assets necessários para a recipe rodar (arquivos, templates, etc.). <a href="http://cookbooks.opscode.com"target="_blank" rel="noopener">Cookbooks</a> são muito frequentemente compartilháveis e já existem muitos deles.</p>
<p>O Chef pode executar seus cookbooks em dois modos — um modo cliente/servidor (chef-client) ou um modo standalone (chef-solo). Quando você executa chef-solo, passa uma URL para um tarball cheio dos cookbooks que você quer rodar. Não há mais infraestrutura do que isso — coloque seus tarballs em algum lugar onde possa baixá-los e se divirta.</p>
<p>No modo cliente/servidor, cada cliente Chef é configurado para se comunicar com um Chef Server. O servidor armazena informações sobre cada cliente (muitas delas — coisas como endereços IP, módulos de kernel carregados e mais) e distribui os cookbooks de que precisam para se configurar. Ele também fornece uma API REST e uma interface Web interativa, para que você possa alterar facilmente a configuração de seus sistemas de forma centralizada. Por fim, todos os dados que o servidor coleta são indexados e pesquisáveis — você pode usar isso em suas recipes para configurar serviços que requerem configuração complexa e dinâmica. (Alguns exemplos seriam descobrir dinamicamente um servidor MySQL master ou encontrar todos os servidores memcached em um cluster)</p>
<p><strong>AkitaOnRails:</strong> O que você diria sobre a maturidade do Chef? O CFEngine tem mais de uma década de uso, o que é difícil de superar. Você diria que é &ldquo;maduro o suficiente&rdquo;? Ou seja, já está em produção em empresas de muitos tamanhos, suas APIs não mudam muito e meus cookbooks provavelmente funcionarão se eu atualizar para uma versão mais nova do Chef?</p>
<p><strong>Opscode:</strong> O Chef tem pouco mais de um ano — foi lançado ao público em 15 de janeiro de 2009. Desde então, 42 desenvolvedores diferentes contribuíram para o <a href="https://www.ohloh.net/p/opscode-chef/factoids/2025809"target="_blank" rel="noopener">projeto</a>, sendo que cerca de 5 trabalham para a Opscode. Está em uso em produção no Engine Yard Cloud desde o dia do lançamento. Desde então, foi adotado por empresas e universidades de todos os tamanhos, de pequenas startups a grandes empresas com requisitos de compliance muito pesados.</p>
<p>Então definitivamente é &ldquo;maduro o suficiente&rdquo; para uso no mundo real — muitas pessoas o usam e dependem dele todos os dias. Equilibrar esse conhecimento com a realidade de que o projeto está em rápida evolução é importante, e tentamos fazer isso de várias formas:</p>
<ol>
<li>A sintaxe de recipe é considerada basicamente &ldquo;completa&rdquo;. Se fizermos mudanças, elas são retrocompatíveis com lançamentos anteriores do Chef. Salvo algo verdadeiramente incrível que altere radicalmente a forma do universo, isso permanecerá verdadeiro.</li>
<li>Testamos muito. O Chef tem mais de 2000 unit tests e functional tests que cobrem toda a API REST e muitos dos recursos individuais (visamos 100% de cobertura de testes de features). Há mais linhas de código testando o Chef do que linhas de código no próprio Chef.</li>
<li>Se de fato quebrarmos compatibilidade retroativa, tentamos fazer isso apenas quando incrementamos a revisão minor. Se você tem uma versão 0.7.x do Chef instalada, ela deve sempre funcionar com outras versões do Chef no mesmo ciclo de lançamento.</li>
</ol>
<p>A maior parte do trabalho no Chef atualmente é sobre adicionar mais funcionalidades, não em mudar a forma como as funcionalidades existentes funcionam. É seguro usar o Chef hoje — no futuro, você continuará recebendo mais ingredientes para adicionar ao mix, em vez de ter que refatorar profundamente a forma como você faz as coisas.</p>
<p>Por fim, a Comunidade Chef é verdadeiramente épica — temos muitas pessoas que dedicam quantidades significativas do seu tempo ao Chef. Mesmo que não estejam contribuindo código, estão respondendo perguntas, escrevendo documentação, ficando no IRC e oferecendo cupcakes. É um grupo de pessoas focadas em resolver problemas do mundo real e ajudar umas às outras a fazer o mesmo. É de longe o que mais me orgulha e acho que tem um impacto significativo em se o Chef está pronto para o prime time. O banco é realmente muito profundo.</p>
<p><strong>AkitaOnRails:</strong> O Chef usa Ruby diretamente, o que alguns diriam ser uma bênção e uma maldição. Provavelmente é perfeito para Rubistas, mas sinto que a maioria dos sysadmins está acostumada com Bash, Python e não é muito flexível a mudanças. Por que você escolheu usar Ruby em vez de uma linguagem mais simples?</p>
<p><strong>Opscode:</strong> A resposta para a pergunta de por que usamos Ruby diretamente para a linguagem de configuração tem duas partes: por que estendemos uma linguagem 3GL em vez de construir uma DSL declarativa ou uma linguagem de modelagem completa, e por que escolhemos Ruby como essa 3GL.</p>
<p>Primeiro, por que estendemos uma 3GL. Ferramentas como Cfengine e Puppet constroem uma DSL declarativa para gerenciamento de configuração — uma linguagem customizada que fornece um modelo dentro do qual administradores de sistemas ou desenvolvedores de software podem trabalhar para automatizar seu sistema. Outras ferramentas como o EDML e ECML da Elastra, o DCML da OpsWare ou o Bcfg2 dão a você um schema XML para descrever como o sistema deve se comportar.</p>
<p>O problema com essas abordagens é que, por definição, elas devem construir uma abstração para cada tarefa que o usuário final pode querer realizar: uma façanha impossível. O nível de complexidade inerente à automação, aliado à incapacidade de recorrer a construções de linguagem 3GL quando necessário, resulta em um sistema que só pode lidar com um subconjunto da complexidade total, em vez de permitir que os usuários encontrem soluções inovadoras para seus problemas específicos. Ao aproveitar o Ruby, adicionar suporte para outros casos de uso é uma questão de adicionar novos conjuntos de classes base mantendo consistência e acessibilidade no design de sua interface, porque o escopo completo da linguagem está disponível.</p>
<p>Nosso objetivo com o Chef era manter a simplicidade que vem de ter o foco na declaração de recursos idempotentes, enquanto dá a você a flexibilidade de uma 3GL completa. Em termos práticos, tudo que você pode fazer com Ruby, você pode fazer com Chef — e como Ruby é uma 3GL, isso equivale a essencialmente qualquer coisa.</p>
<p>Larry Wall, o criador do Perl, tem uma citação que adoro:</p>
<blockquote>
  <p>&ldquo;Para a maioria das pessoas, a utilidade percebida de uma linguagem de computador é inversamente proporcional ao número de eixos teóricos que a linguagem pretende afiar.&rdquo;</p>

</blockquote>
<p>Como queríamos que o Chef tivesse a máxima utilidade, ativamente tentamos remover o maior número possível de eixos teóricos — a crença de que podemos imaginar a amplitude total do espaço do problema sendo um deles. Há mais de uma forma de fazer isso com Chef, e o único critério válido para avaliar o sucesso do seu projeto de automação é se ele resolve seus problemas de forma confiável.</p>
<p>Na prática, você precisa saber muito pouco de Ruby para usar o Chef. Aqui está um exemplo de instalação do programa &ldquo;screen&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">package</span> <span class="s2">&#34;screen&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="n">action</span> <span class="ss">:install</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O mesmo em Puppet:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">package</span> <span class="p">{</span> <span class="s2">&#34;screen&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"> <span class="k">ensure</span> <span class="o">=&gt;</span> <span class="n">present</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E em Cfengine 2:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="ss">packages</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"> <span class="n">screen</span> <span class="n">action</span><span class="o">=</span><span class="n">install</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Embora todos esses sistemas exijam aprender a sintaxe, em um nível base não há muita diferença entre eles em termos de aprendizado bruto necessário. A diferença é que quando você atinge uma limitação no Chef, você tem a capacidade de inovar facilmente, e quando atinge as mesmas limitações em outras ferramentas, você não tem.</p>
<p>Escolhemos Ruby por sua capacidade bastante única de criar nova sintaxe facilmente. Ferramentas como Rspec são bons exemplos de formas como você pode manipular Ruby de forma útil e que são muito difíceis de replicar em outras ferramentas. Queríamos garantir que, mesmo estando &ldquo;em&rdquo; uma 3GL, você não precisasse passar por obstáculos adicionais para fazer as coisas simples funcionarem. Ruby era a linguagem em que eu me sentia suficientemente confortável e que eu sabia ter a capacidade de tornar isso realidade.</p>
<p>Dito isso, um de nossos objetivos é estender a capacidade de escrever recipes Chef para outras 3GLs. Já temos um exemplo disso com Perl — e não fizemos nenhuma mudança no código fonte do Chef para isso. Você pode ver o <a href="http://search.cpan.org/~holoway/Chef-0.01/lib/Chef.pm"target="_blank" rel="noopener">demo aqui no CPAN</a>. Funciona usando a API JSON do Chef para enviar os recursos compilados para a biblioteca Ruby para execução, e ao longo do tempo estaremos estendendo essas interfaces. Você poderá ter recipes escritas em Python interoperando com recipes escritas em Ruby e Perl.</p>
<p><strong>AkitaOnRails:</strong> O Chef tem (ou precisa de) algo como o Augeas, que o Puppet está tentando suportar?</p>
<p><strong>Opscode:</strong> O Augeas é interessante. O Chef não tem suporte ao Augeas hoje, e a razão é que ninguém precisou tanto a ponto de escrever a integração. Uma razão é que, com o Chef, é bastante fácil adicionar dinamicamente a um recurso (como, por exemplo, um template) ou pesquisar sistemas específicos que correspondam a um critério. Isso significa que o caso de uso do Augeas (que edita arquivos in-place) é menos necessário — você muitas vezes pode obter os dados necessários para renderizar um template, em vez de precisar construí-lo ao longo do tempo com edições incrementais.</p>
<p>Achamos que isso é uma prática melhor em geral, pois garante que todos os sistemas que você está gerenciando sempre possam ser restaurados a um estado funcional a partir apenas do repositório de cookbooks. Se você usa o Augeas para permitir mudanças idempotentes de linhas individuais de um arquivo de configuração, ele incentiva o comportamento de administradores individuais editando arquivos in-place, o que é um anti-padrão de gerenciamento de configuração.</p>
<p><strong>AkitaOnRails:</strong> Sysadmins acostumados ao CFEngine reclamam das dependências do Ruby e do peso geral. Porque para o Chef rodar você precisa do Ruby instalado. Nem todas as distros têm Ruby na mesma versão (embora a maioria já tenha migrado para 1.8.7). Depois há o problema do peso. Não estou familiarizado com o Chef, mas o Puppet pode chegar a centenas de megabytes. O que não querem é ter clusters de máquinas Chef (que, por si só, também precisam de manutenção, adicionando à complexidade geral). Como vocês lidam com datacenters com milhares de servidores? Sei que é difícil medir com precisão, mas qual seria uma proporção razoável entre servidores Chef x servidores gerenciados?</p>
<p><strong>Opscode:</strong> Ao avaliar a escalabilidade de sistemas de gerenciamento de configuração, você quer analisar dois eixos diferentes. O horizontal, que é o número de sistemas que podem ser gerenciados por um único servidor de gerenciamento de configuração a uma taxa específica de mudança, e o vertical, que é quanto da sua infraestrutura pode ser automatizada pela ferramenta.</p>
<p>No eixo vertical, acho que o Chef é o claro vencedor, por razões que acho bem resumidas pela minha resposta à pergunta 7. Colocaria o Puppet em segundo lugar e o Cfengine por último.</p>
<p>No eixo horizontal, o Cfengine é o claro vencedor. É escrito em C e tem o componente de servidor mais fino possível — basicamente não faz nada além de autenticar clientes e servir arquivos. Conheço em primeira mão datacenters que estão rodando enormes quantidades de servidores com um único servidor cfengine.</p>
<p>Uma métrica importante a ter em mente ao discutir a escalabilidade horizontal de uma solução de gerenciamento de configuração é que a métrica mais importante é a <strong>taxa de mudança</strong>. Todas as ferramentas sobre as quais estamos falando são baseadas em &lsquo;pull&rsquo; — os clientes fazem check-in em um &lsquo;intervalo&rsquo; com o servidor e aplicam um <em>&lsquo;splay&rsquo;</em> para garantir que nem todos os sistemas façam check-in de uma vez. Uma configuração out-of-the-box comum é um intervalo de 30 minutos com um splay de 15 minutos. (Isso significa que um servidor faz check-in a cada 30-45 minutos, dependendo.) Se você se sentir confortável em aumentar esse intervalo, obterá mais escalabilidade com menos recursos (reduzindo a quantidade de concorrência).</p>
<p>Então, quando alguém disser &ldquo;tenho 10 mil servidores em um único servidor de gerenciamento de configuração&rdquo;, pergunte &ldquo;a que intervalo?&rdquo;.</p>
<p>O Chef escala como uma aplicação web. O próprio servidor é bastante leve e é responsável por autenticar clientes, transferir arquivos, armazenar o estado dos nós e fornecer uma interface de busca. Ele escala horizontalmente adicionando novos Servidores Chef conforme necessário. A API é RESTful e não há estado de sessão entre os clientes e o servidor (pelo menos não no Chef 0.8+). Quando você encontra problemas de escalabilidade com o Chef, as ferramentas que você aplica são as mesmas que você aplica a qualquer aplicação web bem projetada.</p>
<p>Você perguntou especificamente sobre utilização de memória — o Chef se sai muito bem nesse aspecto. Os processos de servidor individuais geralmente têm entre 14-50MB residente. O próprio cliente, rodando em modo daemon, geralmente fica em torno de 28MB residente.</p>
<p>Em nossos testes, o gargalo atual em um servidor Chef é CPU. O Chef é inteligente sobre como lida com transferências de arquivos — transferimos apenas arquivos que mudaram no servidor. Para suportar isso, calculamos um checksum para cada arquivo solicitado e atualmente não armazenamos os resultados em cache. Planejamos corrigir isso para o próximo lançamento principal do Chef (0.8.0), o que deve mudar o gargalo para a RAM.</p>
<p>Com o Chef 0.7, você deve ser capaz de suportar milhares de clientes a um intervalo de meia hora e splay de quinze minutos em hardware commodity razoável. As mudanças no Chef 0.8 devem elevar esse número dramaticamente — voltarei com alguns benchmarks assim que os patches estiverem prontos. :)</p>
<p><strong>AkitaOnRails:</strong> Provavelmente relacionado à pergunta anterior: parece que especialmente após a Sarbanes-Oxley houve um interesse crescente em coisas como ITIL e CoBit. Você já viu implementações bem-sucedidas desses em infraestrutura no estilo Web? Quero dizer, posso vê-los funcionando em Bancos, Aeroespacial e Defesa, etc., mas não consigo ver funcionando conforme anunciado em um ambiente muito dinâmico como hospedagem de serviços Web. Quais são suas experiências com essa questão?</p>
<p><strong>Opscode:</strong> Bem, acho que você pode pensar em ITIL da mesma forma que pensa no modelo clássico &lsquo;Waterfall&rsquo; de desenvolvimento de software. Para alguns tipos de projetos e empresas, é essencial — é difícil imaginar trabalhar de outra forma. Na maioria das vezes, são empresas com grandes preocupações de fabricação ou controle de qualidade — saúde médica, aeroespacial, bancário e financeiro, etc.</p>
<p>O mesmo se aplica ao ITIL — quanto maior a organização, mais rigorosos os requisitos e maior o prazo de entrega, mais os processos que descrevem começam a fazer sentido. Como todos os grandes processos, porém, tendem a diminuir o elemento humano — as pessoas têm papéis a desempenhar e formulários a preencher. :)</p>
<p>Na cultura de Web Ops, as coisas são diferentes. Nunca vi um casamento bem-sucedido entre ITIL e Web Ops, e a razão é que os domínios são muito diferentes. Se há um bug no site, é melhor enviar a correção agora do que esperar por um processo de gestão de lançamento para garantir que o site não terá mais problemas com base na sua correção, por exemplo. Também é um ajuste cultural ruim — nas melhores equipes de Web Ops, o foco é intenso em comunicação, agilidade e respeito, em vez de processo, formalismo e ferramentas. A mudança que você começa a ver nas grandes empresas de Web Ops é que seu pessoal de operações se torna habilitador da organização, em vez de bloqueadores de linha final de mudanças (para manter a estabilidade alta).</p>
<p>Em geral, acho que os 4 passos descritos no visible ops para gestão de emergências não são ruins, mas o diabo está sempre nos detalhes. A pessoa que você deveria realmente perguntar sobre isso é Jesse — ele é em grande parte responsável pela cultura operacional da Amazon e sabe o que significa começar a hackear esse tipo de coisa de dentro de grandes organizações.</p>
<p><strong>AkitaOnRails:</strong> Incrível, acho que é isso. Muito obrigado por esta entrevista!</p>
]]></content:encoded><category>entrevistas</category><category>devops</category><category>automacao</category></item><item><title>Conversando com Luke Kanies</title><link>https://akitaonrails.github.io/2009/11/18/conversando-com-luke-kanies/</link><guid isPermaLink="true">https://akitaonrails.github.io/2009/11/18/conversando-com-luke-kanies/</guid><pubDate>Wed, 18 Nov 2009 11:45:00 -0200</pubDate><description>&lt;p&gt;Gerenciamento de Configuração é um assunto complicado. Para quem não está familiarizado: quando você é um desenvolvedor com poucos servidores para cuidar, geralmente dá para se virar gerenciando-os manualmente. As pessoas provavelmente estão acostumadas a colocar um CD, dar duplo clique no programa &amp;ldquo;instalar&amp;rdquo; e clicar &amp;ldquo;avançar&amp;rdquo;, &amp;ldquo;avançar&amp;rdquo; até o fim, depois entrar manualmente para fazer backup (quando se lembra), e às vezes até aplicar atualizações de segurança quando se lembra delas.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Gerenciamento de Configuração é um assunto complicado. Para quem não está familiarizado: quando você é um desenvolvedor com poucos servidores para cuidar, geralmente dá para se virar gerenciando-os manualmente. As pessoas provavelmente estão acostumadas a colocar um CD, dar duplo clique no programa &ldquo;instalar&rdquo; e clicar &ldquo;avançar&rdquo;, &ldquo;avançar&rdquo; até o fim, depois entrar manualmente para fazer backup (quando se lembra), e às vezes até aplicar atualizações de segurança quando se lembra delas.</p>
<p>Mas quando você tem mais de uma dúzia de máquinas, as coisas começam a ficar feias. Você acaba cometendo mais erros, esquecendo etapas importantes, e de repente gerenciar máquinas vira um pesadelo. Você acaba sendo acordado no meio da noite porque esqueceu de instalar algum componente crucial, e por aí vai.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/pic2_original.jpg" alt=""  loading="lazy" /></p>
<p>Da mesma forma que você precisa de testes e ferramentas de integração contínua como desenvolvedor, você também precisa de ferramentas automatizadas, confiáveis e flexíveis para o papel de administrador de sistemas. É aí que ferramentas como o <strong>Puppet</strong> entram para ajudar.</p>
<p>Desta vez entrevistei <a href="http://twitter.com/puppetmasterd"target="_blank" rel="noopener">Luke Kanies</a>, do <a href="http://reductivelabs.com/"target="_blank" rel="noopener">Reductive Labs</a>, ex-contribuidor da famosa ferramenta CFEngine e criador do <a href="http://github.com/reductivelabs/puppet"target="_blank" rel="noopener">Puppet</a>, uma das ferramentas de gerenciamento de configuração mais aclamadas para datacenters do século XXI.</p>
<p><strong>AkitaOnRails:</strong> Para começar esta entrevista, seria ótimo ter mais informações de background sobre você. Como você chegou ao campo de gerenciamento de configuração? Entendo que você tem uma longa história com o desenvolvimento do CFEngine, certo?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/luke_kanies_portrait_original.jpg" alt=""  loading="lazy" /></p>
<p><strong>Luke:</strong> Fui administrador Unix desde 1997, sempre escrevendo scripts e ferramentas para economizar tempo, e por volta de 2001 percebi que não deveria precisar escrever tudo sozinho — que alguém em algum lugar deveria ser capaz de me poupar tempo. Após muita pesquisa e experimentação, me fixei no Cfengine e tive sucesso suficiente com ele para começar a fazer consultoria, publicar e contribuir para o projeto.</p>
<p><strong>AkitaOnRails:</strong> Qual é a história por trás do Reductive Labs, qual é sua missão e qual é a história da criação do Puppet?</p>
<p><strong>Luke:</strong> Depois de alguns anos com o Cfengine, tinha muito mais insight, mas estava frustrado porque ainda parecia difícil demais — ninguém estava compartilhando código Cfengine e havia alguns problemas que exigiam muito trabalho para resolver. O maior problema, porém, era que seu desenvolvimento era muito fechado — era difícil contribuir com muito mais do que apenas correções de bugs.</p>
<p>Fiquei frustrado o suficiente para parar de fazer consultoria e procurar outras opções. Trabalhei brevemente no BladeLogic, uma empresa de software comercial nesse espaço, mas no final decidi que o insight que tinha e a falta de uma ótima solução eram um bom começo para um negócio — então decidi transformar minha empresa de consultoria em uma empresa de software e escrever uma nova ferramenta.</p>
<p><strong>AkitaOnRails:</strong> Gostaria de dizer que apenas sysadmins amadores fazem tudo manualmente, mas acho que a maioria das corporações pequenas e médias ainda faz tudo manualmente ou com scripts aleatórios espalhados por todo lugar. A noção de &ldquo;gerenciamento de configuração&rdquo; ainda é nova para muita gente. Você poderia explicar brevemente o que é e por que é importante?</p>
<p><strong>Luke:</strong> É surpreendentemente difícil descrever de forma concisa, mas para mim há duas regras principais: você não deveria precisar se conectar diretamente a uma máquina para alterar sua configuração, e deveria ser capaz de reimplantar qualquer máquina na sua rede muito rapidamente.</p>
<p>Essas duas regras combinadas exigem automação e/ou centralização abrangente de tudo que é necessário para fazer uma máquina funcionar. Irritantemente, elas também introduzem imediatamente ciclos de dependência, porque o seu servidor de automação precisa ser capaz de se construir, o que sempre é um pequeno desafio.</p>
<p><strong>AkitaOnRails:</strong> Acho que um dos sistemas mais amplamente utilizados é o CFEngine2. Como o Puppet se compara a ele? Ou seja, qual o valor agregado ao migrar para o Puppet e quais são as ressalvas conhecidas?</p>
<p><strong>Luke:</strong> Há várias diferenças funcionais importantes. A maior é a <strong>Resource Abstraction Layer</strong> do Puppet, que permite que os usuários do Puppet evitem muitos detalhes com os quais não se preocupam realmente, como como rpm, adduser ou init scripts funcionam — eles falam sobre usuários, grupos e pacotes, e o Puppet descobre os detalhes.</p>
<p>Também temos suporte explícito a dependências, o que faz uma enorme diferença — é fácil ordenar recursos relacionados e reiniciar serviços quando seus arquivos de configuração mudam, por exemplo.</p>
<p>A linguagem também é um pouco mais poderosa. Como o Cfengine, temos uma linguagem customizada simples, mas a linguagem do Puppet fornece melhor suporte para heterogeneidade, além de uma construção de recurso composto que permite criar facilmente tipos de recursos em linguagem como virtual hosts Apache que modelam recursos mais complexos compostos por múltiplos recursos simples.</p>
<p><strong>AkitaOnRails:</strong> O Puppet tem muitos componentes. Você pode descrever brevemente alguns dos principais que trabalham juntos? O lado do cliente, o lado do servidor, as recipes?</p>
<p><strong>Luke:</strong> A maioria das pessoas usa o Puppet no modo cliente/servidor, onde o servidor central é a única máquina que tem acesso a todo o código, e ele executa um processo capaz de compilar esse código em configurações específicas para cada host. Então cada máquina executa um cliente (incluindo o servidor), que recupera e aplica essa configuração específica do host. Isso tem boas implicações de segurança porque você não enviou seu código para cada máquina na rede.</p>
<p>Se esse modelo não funcionar para você, também é fácil executar o Puppet de forma standalone, onde cada máquina tem todo o código e o compila separadamente. Vários usuários do Puppet fazem isso por várias razões. Esse executável &lsquo;puppet&rsquo; standalone é um interpretador padrão — pode ser usado para executar scripts de 1 linha ou milhares de linhas em uma configuração completa.</p>
<p>Além disso, temos alguns outros executáveis interessantes, como para acessar nossa funcionalidade de autoridade de certificação, e um executável interessante chamado &lsquo;ralsh&rsquo; que fornece uma forma simples de gerenciar recursos diretamente do shell Unix, sem precisar escrever um script separado.</p>
<p><strong>AkitaOnRails:</strong> O que você diria sobre a maturidade do Puppet? O CFEngine tem mais de uma década de uso, o que é difícil de superar. Você diria que é &ldquo;maduro o suficiente&rdquo;? Ou seja, já está em produção em empresas de muitos tamanhos, suas APIs não mudam muito e minhas recipes provavelmente funcionarão se eu atualizar para uma versão mais nova do Puppet? Acho que a versão 0.x deixa algumas pessoas nervosas :-)</p>
<p><strong>Luke:</strong> Na verdade deveríamos ter chamado uma versão de 2007 de 1.0, mas é difícil saber o quão estável um lançamento será até que ele esteja disponível há algum tempo. :)</p>
<p>É obviamente difícil competir com a longa vida do Cfengine, embora eles estejam de certa forma migrando forçosamente para o Cfengine 3, que é uma reescrita completa, então essa maturidade não vale tanto assim agora.</p>
<p>No entanto, o Puppet está em uso em produção ao redor do mundo desde 2006 e atualmente é usado por mais grandes empresas do que eu poderia razoavelmente nomear — Twitter, Digg, Google, Sun, Red Hat e muitas outras — e nossa comunidade e base de clientes o consideram maduro. A virada aconteceu em algum momento no início do ano passado, quando descobri que a grande maioria dos problemas das pessoas eram problemas do lado deles, em vez de alguma falha ou limitação no Puppet.</p>
<p>Em geral as APIs são bastante estáveis e acho que nos saímos muito bem em manter a compatibilidade retroativa quando as APIs precisaram mudar. O ponto sobre estabilidade de API em um lançamento 1.0 não é tanto diferenciá-lo de esforços anteriores quanto fazer uma promessa para o futuro. Isso importa especialmente para empresas como a Canonical, que querem um lançamento que possam suportar no Ubuntu por cinco anos.</p>
<p><strong>AkitaOnRails:</strong> O Puppet tem sua própria linguagem e você pode usar Ruby para os casos avançados. Provavelmente é perfeito para Rubistas, mas sinto que a maioria dos sysadmins está acostumada com Bash, Python e não é muito flexível a mudanças. Por que você escolheu usar Ruby em vez de uma linguagem mais difundida? O que os sysadmins precisam perceber para mudar de paradigmas?</p>
<p><strong>Luke:</strong> Parte disso é que a maioria das pessoas não precisa realmente saber nada de Ruby para ser eficaz com o Puppet. Claro, você pode ter mais poder se souber, mas se você não é uma pessoa de linguagens, funciona perfeitamente com apenas Puppet.</p>
<p>Outra coisa boa é que temos uma escala bastante suave em termos de conhecimento de Ruby — você pode começar escrevendo templates ERB ou extensões de cinco linhas para o <a href="http://github.com/reductivelabs/facter"target="_blank" rel="noopener">Facter</a>, que é nosso sistema de consulta do lado do cliente, e crescer suavemente até escrever tipos de recursos customizados.</p>
<p>No fim das contas, escolhi Ruby porque era mais produtivo nele. Provavelmente deveria ter escolhido Python, dado seu benefício de velocidade e popularidade no Red Hat e outros lugares, mas descobri que simplesmente não conseguia escrever código nele. Comecei a pensar em Ruby depois de apenas algumas horas de uso, então era impossível para mim me afastar dele.</p>
<p><strong>AkitaOnRails:</strong> Sysadmins acostumados ao CFEngine reclamam das dependências do Ruby e do peso geral. Porque para o Puppet rodar você precisa do Ruby instalado. Nem todas as distros têm Ruby na mesma versão (embora a maioria já tenha migrado para 1.8.7). Depois há o problema do peso. O Puppet pode chegar a centenas de megabytes. O que não querem é ter clusters de máquinas Puppet (que, por si só, também precisam de manutenção, adicionando à complexidade geral). Como você lida com datacenters com milhares de servidores? Sei que é difícil medir com precisão, mas qual seria uma proporção razoável entre servidores Puppet x servidores gerenciados?</p>
<p><strong>Luke:</strong> É tão impossível te dizer quantos clientes um servidor Puppet pode atender quanto é te dizer quantos clientes um servidor Rails pode atender — tudo depende da complexidade das tarefas. O Google escalou para 4500 máquinas clientes em um único servidor, mas a maioria das pessoas tende a adicionar outro servidor em torno de 500-1000 clientes.</p>
<p>É verdade que é difícil manter o uso de memória baixo em um processo Ruby, mas fizemos grandes avanços nos nossos lançamentos recentes fazendo coisas como deduplicar strings em memória e ser mais eficiente em nossos code paths. Mas na verdade gastamos muito mais tempo em features e correções de bugs e menos tempo em otimização — até recentemente éramos uma equipe de desenvolvimento pequena e simplesmente não tínhamos largura de banda para isso.</p>
<p>Agora que minha empresa, Reductive Labs, tem algum investimento, conseguimos adicionar três desenvolvedores em tempo integral, o que vai ajudar muito nessa área.</p>
<p>Quanto a dependências, essa é uma área onde nos afastamos fortemente da comunidade Ruby — não exigimos uma única gem, a não ser nossa própria ferramenta Facter (e normalmente não é enviada como gem). Os Rubistas tendem a não se preocupar muito com dependências de pacotes — eles simplesmente colocam no vendor, como gosto de dizer — mas isso não funciona quando você tem que implantar milhares de cópias. Então sim, você pode ter que instalar Ruby, mas não haverá outras dependências com as quais você tenha que lidar, o que simplifica muito.</p>
<p>Geralmente é tão difícil saber como você precisará dimensionar seu puppetmaster quanto seria dimensionar um servidor web — depende de quão complicada é a carga de trabalho. Em geral, em algum ponto entre 500 e 5000 clientes, você precisará de um segundo servidor, mas a maioria das pessoas provavelmente encontra isso mais perto de 500. Mas, se você tem 3000 clientes acessando um serviço, provavelmente quer torná-lo horizontalmente escalável para estabilidade além de performance.</p>
<p><strong>AkitaOnRails:</strong> Segurança é uma grande preocupação hoje em dia. O Puppet se preocupou desde o início com o procedimento de handshake entre clientes e servidor. Você pode descrevê-lo um pouco? Também existe alguma recipe embutida para hardening de máquinas, por exemplo? Ou pelo menos algum desejo de adicionar tais ferramentas no futuro?</p>
<p><strong>Luke:</strong> O Puppet usa certificados SSL padrão para autenticação e criptografia, incluindo uma fase de assinatura de certificados. Por padrão, o cliente gera uma chave e uma requisição de certificado (CSR) e depois envia o CSR para seu servidor. Esse envio, junto com o download posterior do certificado, são as únicas conexões não autenticadas permitidas por padrão.</p>
<p>A partir daí, um humano normalmente precisa acionar a assinatura do certificado do cliente, mas muitas organizações, incluindo o Google, assinam automaticamente os certificados dos clientes porque confiam em sua rede interna.</p>
<p>Quanto ao hardening automático, não há recipes que eu saiba agora, mas é algo que definitivamente me interessa. Anos atrás era grande fã do TITAN, que é um pacote de hardening para várias plataformas *NIX, e foi parte da inspiração para escrever o Puppet — sempre quis uma política de segurança portável e executável.</p>
<p><strong>AkitaOnRails:</strong> O puppetmaster usa Webrick por padrão, mas a documentação também descreve o uso do Mongrel ou Passenger. Há ganhos reais em usá-los? É mais para conveniência ou temos melhorias de performance/robustez?</p>
<p><strong>Luke:</strong> Nossa, o Webrick é lento. É realmente fantástico para provas de conceito — sobe e roda em minutos. Uma vez que você passa dessa prova de conceito, porém, você realmente precisa mudar para Mongrel ou Passenger. Se você tiver mais de uma conexão concorrente no Webrick, seus clientes começam a sofrer, mas você pode escalar para muito mais com as outras soluções disponíveis.</p>
<p><strong>AkitaOnRails:</strong> Há algum caso de clientes sobre o qual você pode falar? Ou seja, mais detalhes sobre o tipo de infraestrutura, dificuldades, ressalvas, boas práticas?</p>
<p><strong>Luke:</strong> As possibilidades aqui são bastante abertas. O Google usa o Puppet para manter seu TI corporativo, o que significa que o estão executando em milhares de laptops e desktops — o que é bem diferente. A MessageOne, uma divisão da Dell, é realmente interessante porque seus desenvolvedores têm que enviar código Puppet para gerenciar as aplicações que enviam, então se um app não está aparando seus logs ou fazendo backup de si mesmo, é um bug que o desenvolvedor do app tem que corrigir em vez do sysadmin. Isso realmente ajuda a fazer a ponte entre dev e ops, o que funcionou muito bem para eles.</p>
<p>Fora isso, há muitas histórias e boas práticas, mas temo que isso seria um segundo artigo completo. :)</p>
<p><strong>AkitaOnRails:</strong> Já vi Andrew Shafer falar sobre <a href="http://www.slideshare.net/littleidea/agile-infra-agileroots-2009"target="_blank" rel="noopener">Agile Infrastructure</a> por alguns anos, mas ainda acho que a maioria das organizações de TI desconhece esse conceito. Você pode elaborar o que significa ser Ágil fora do campo de desenvolvimento?</p>
<!-- dead Flash embed removed (service offline): flash -->
<p><strong>Luke:</strong> Acho que a Infraestrutura Ágil tem ainda menos adoção do que o Desenvolvimento Ágil. A grande maioria dos departamentos de TI não mudou as práticas significativamente em anos e está amplamente despreparada para o crescimento no número de servidores que estão experimentando. Eles tentam principalmente escalar adicionando mais pessoas, que chamamos de <em>meatcloud</em>, em vez de escalar suas ferramentas e práticas.</p>
<p><strong>AkitaOnRails:</strong> Provavelmente relacionado à pergunta anterior: parece que especialmente após a Sarbanes-Oxley houve um interesse crescente em coisas como ITIL e CoBit. Você já viu implementações bem-sucedidas dessas em infraestrutura no estilo Web? Quero dizer, posso vê-las funcionando em Bancos, Aeroespacial e Defesa, etc., mas não consigo ver funcionando conforme anunciado em um ambiente muito dinâmico como hospedagem de serviços Web. Quais são suas experiências com essa questão?</p>
<p><strong>Luke:</strong> Em geral, acho que esses tipos de políticas de alto nível são ótimas para fins regulatórios, mas não são tão boas para realmente resolver problemas. Quanto maior a empresa e mais pública ela for, mais provável é que se importem com ITIL e similares, mas na minha experiência isso não as ajuda realmente a resolver problemas além do aspecto de relações públicas. Você pode ser compatível com ITIL e disfuncional, ou completamente fora de conformidade, mas em ótima forma. Considerando que os melhores padrões são derivados de implementação e boas práticas — o que poucos desses são —, não tenho muita esperança de que eles sejam adotados pelas melhores equipes por aí.</p>
<p>Minha experiência pessoal é que muito poucas empresas perguntam ou se importam com esses padrões, e as que se importam geralmente o fazem de uma forma de checklist — querem garantir que podem marcar coisas como CMDB, mas não estão realmente preocupadas com os detalhes.</p>
<p><strong>AkitaOnRails:</strong> Acho que é isso! Muito obrigado por esta conversa!</p>
]]></content:encoded><category>entrevistas</category><category>devops</category><category>automacao</category></item></channel></rss>