Major Haydenhttps://major.io/Recent content on Major HaydenHugoenmajor@mhtx.net (Major Hayden)major@mhtx.net (Major Hayden)All content licensed [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ๐Ÿ’œMon, 06 Oct 2025 21:46:10 +0000Fun with doclinghttps://major.io/p/fun-with-docling/Fri, 26 Sep 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/fun-with-docling/<p>My team at work does lots of work with retrieval-augmented generation, or RAG, and parsing documents is really painful. It&rsquo;s so painful that I recently delivered a talk on this very topic at <a href="https://major.io/p/devconf-rag/">DevConf.US 2025</a>!</p> <p>Another one of the talks at DevConf.US this year was about <a href="https://docling-project.github.io/docling/" target="_blank" rel="noreferrer">docling</a>. We&rsquo;ve been using it on our team for a while and we really enjoy how it takes challenging documents in various formats and parses them into a single, common document.</p> <p>I&rsquo;ll walk you through setting up docling in your project and show you some fun things you can do with it.</p> <h1 id="adding-docling-to-a-project" class="relative group">Adding docling to a project <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#adding-docling-to-a-project" aria-label="Anchor">#</a></span></h1><p>I usually use <a href="https://github.com/astral-sh/uv" target="_blank" rel="noreferrer">uv</a> for my Python projects. You can start a new project like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pipx install uv </span></span><span class="line"><span class="cl">mkdir fun-with-docling </span></span><span class="line"><span class="cl"><span class="nb">cd</span> fun-with-docling </span></span><span class="line"><span class="cl">uv init --package --name doclingfun </span></span></code></pre></div><p>Now we can add docling to the project. But first, I prefer to install <code>torch-cpu</code> to avoid downloading lots of unnecessary CUDA libraries if I don&rsquo;t need them.</p> <p>Start by adding a <code>torch-cpu</code> source in your <code>pyproject.toml</code> file:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">tool</span><span class="p">.</span><span class="nx">uv</span><span class="p">.</span><span class="nx">index</span><span class="p">]]</span> </span></span><span class="line"><span class="cl"><span class="nx">name</span> <span class="p">=</span> <span class="s2">&#34;torch-cpu&#34;</span> </span></span><span class="line"><span class="cl"><span class="nx">url</span> <span class="p">=</span> <span class="s2">&#34;https://download.pytorch.org/whl/cpu&#34;</span> </span></span><span class="line"><span class="cl"><span class="nx">explicit</span> <span class="p">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tool</span><span class="p">.</span><span class="nx">uv</span><span class="p">.</span><span class="nx">sources</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="nx">torch</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">index</span> <span class="p">=</span> <span class="s2">&#34;torch-cpu&#34;</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="nx">torchvision</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">index</span> <span class="p">=</span> <span class="s2">&#34;torch-cpu&#34;</span> <span class="p">}</span> </span></span></code></pre></div><p>Then add <code>torch</code>, <code>torchvision</code>, and <code>docling</code> to your dependencies:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv add torch torchvision docling </span></span></code></pre></div><p>Verify the installation:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">&gt; uv run docling --version </span></span><span class="line"><span class="cl">2025-09-26 11:02:48,742 - INFO - Loading plugin <span class="s1">&#39;docling_defaults&#39;</span> </span></span><span class="line"><span class="cl">2025-09-26 11:02:48,743 - INFO - Registered ocr engines: <span class="o">[</span><span class="s1">&#39;easyocr&#39;</span>, <span class="s1">&#39;ocrmac&#39;</span>, <span class="s1">&#39;rapidocr&#39;</span>, <span class="s1">&#39;tesserocr&#39;</span>, <span class="s1">&#39;tesseract&#39;</span><span class="o">]</span> </span></span><span class="line"><span class="cl">Docling version: 2.54.0 </span></span><span class="line"><span class="cl">Docling Core version: 2.48.2 </span></span><span class="line"><span class="cl">Docling IBM Models version: 3.9.1 </span></span><span class="line"><span class="cl">Docling Parse version: 4.5.0 </span></span><span class="line"><span class="cl">Python: cpython-313 <span class="o">(</span>3.13.3<span class="o">)</span> </span></span><span class="line"><span class="cl">Platform: Linux-6.16.8-200.fc42.x86_64-x86_64-with-glibc2.41 </span></span></code></pre></div><h2 id="parsing-a-document" class="relative group">Parsing a document <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#parsing-a-document" aria-label="Anchor">#</a></span></h2><p>Financial markets are a hobby of mine, so I always enjoy reading research on patterns in the market. There&rsquo;s a <a href="https://arxiv.org/abs/2509.16137" target="_blank" rel="noreferrer">great document</a> that we can start with. Download the PDF and parse it with docling:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -L -o enhancing_ohlc_data.pdf https://arxiv.org/pdf/2509.16137 </span></span><span class="line"><span class="cl">&gt; uv run docling enhancing_ohlc_data.pdf --from pdf --to json --output . </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,958 - INFO - Loading plugin <span class="s1">&#39;docling_defaults&#39;</span> </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,959 - INFO - Registered ocr engines: <span class="o">[</span><span class="s1">&#39;easyocr&#39;</span>, <span class="s1">&#39;ocrmac&#39;</span>, <span class="s1">&#39;rapidocr&#39;</span>, <span class="s1">&#39;tesserocr&#39;</span>, <span class="s1">&#39;tesseract&#39;</span><span class="o">]</span> </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,964 - INFO - paths: <span class="o">[</span>PosixPath<span class="o">(</span><span class="s1">&#39;/tmp/tmp243lux62/enhancing_ohlc_data.pdf&#39;</span><span class="o">)]</span> </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,964 - INFO - detected formats: <span class="o">[</span>&lt;InputFormat.PDF: <span class="s1">&#39;pdf&#39;</span>&gt;<span class="o">]</span> </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,971 - INFO - Going to convert document batch... </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,971 - INFO - Initializing pipeline <span class="k">for</span> StandardPdfPipeline with options <span class="nb">hash</span> f1301fa0db91f613a1f4baa1a2a11518 </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,973 - INFO - Loading plugin <span class="s1">&#39;docling_defaults&#39;</span> </span></span><span class="line"><span class="cl">2025-09-26 11:07:59,974 - INFO - Registered picture descriptions: <span class="o">[</span><span class="s1">&#39;vlm&#39;</span>, <span class="s1">&#39;api&#39;</span><span class="o">]</span> </span></span><span class="line"><span class="cl">2025-09-26 11:08:00,241 - INFO - Accelerator device: <span class="s1">&#39;cpu&#39;</span> </span></span><span class="line"><span class="cl">2025-09-26 11:08:01,278 - INFO - Accelerator device: <span class="s1">&#39;cpu&#39;</span> </span></span><span class="line"><span class="cl">2025-09-26 11:08:05,085 - INFO - Accelerator device: <span class="s1">&#39;cpu&#39;</span> </span></span><span class="line"><span class="cl">2025-09-26 11:08:05,381 - INFO - Processing document enhancing_ohlc_data.pdf </span></span><span class="line"><span class="cl">2025-09-26 11:08:51,604 - INFO - Finished converting document enhancing_ohlc_data.pdf in 51.64 sec. </span></span><span class="line"><span class="cl">2025-09-26 11:08:51,604 - INFO - writing JSON output to enhancing_ohlc_data.json </span></span><span class="line"><span class="cl">2025-09-26 11:08:51,667 - INFO - Processed <span class="m">1</span> docs, of which <span class="m">0</span> failed </span></span><span class="line"><span class="cl">2025-09-26 11:08:51,672 - INFO - All documents were converted in 51.71 seconds. </span></span></code></pre></div><p>Success! We now have a JSON file that contains the parsed document. (If you&rsquo;re in a hurry and just want to view the JSON, I&rsquo;ve <a href="enhancing_ohlc_data.json">uploaded it here</a>.)</p> <p>Let&rsquo;s break down how the documents work</p> <h1 id="groups-and-texts" class="relative group">Groups and texts <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#groups-and-texts" aria-label="Anchor">#</a></span></h1><p>Groups are collections of related content. Here&rsquo;s an example:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;groups&#34;</span><span class="err">:</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;self_ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/groups/0&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;parent&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;$ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/body&#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;children&#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;$ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/texts/53&#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;$ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/texts/54&#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;$ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/texts/55&#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;$ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/texts/56&#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;content_layer&#34;</span><span class="p">:</span> <span class="s2">&#34;body&#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;list&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;label&#34;</span><span class="p">:</span> <span class="s2">&#34;list&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">},</span> </span></span></code></pre></div><p>This group has four children which are called &ldquo;texts&rdquo; and we can tell that this is a list of some sort.</p> <p>If we look for <code>#/texts/53</code>, we can see what the first item in the list is:</p> <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;self_ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/texts/53&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;parent&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;$ref&#34;</span><span class="p">:</span> <span class="s2">&#34;#/groups/0&#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;children&#34;</span><span class="p">:</span> <span class="p">[],</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;content_layer&#34;</span><span class="p">:</span> <span class="s2">&#34;body&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;label&#34;</span><span class="p">:</span> <span class="s2">&#34;list_item&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;prov&#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;page_no&#34;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;bbox&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;l&#34;</span><span class="p">:</span> <span class="mf">86.945</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;t&#34;</span><span class="p">:</span> <span class="mf">669.104</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;r&#34;</span><span class="p">:</span> <span class="mf">540.004</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;b&#34;</span><span class="p">:</span> <span class="mf">644.616</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;coord_origin&#34;</span><span class="p">:</span> <span class="s2">&#34;BOTTOMLEFT&#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;charspan&#34;</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="mi">0</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="mi">187</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;orig&#34;</span><span class="p">:</span> <span class="s2">&#34;\u00b7 Market relevance : VWAP is widely followed by institutional investors, trading algorithms, and benchmark providers [6] [2]. As such, it is a meaningful and valuable quantity to predict.&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;text&#34;</span><span class="p">:</span> <span class="s2">&#34;Market relevance : VWAP is widely followed by institutional investors, trading algorithms, and benchmark providers [6] [2]. As such, it is a meaningful and valuable quantity to predict.&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;enumerated&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;marker&#34;</span><span class="p">:</span> <span class="s2">&#34;\u00b7&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span><span class="err">,</span> </span></span></code></pre></div><p>This is a single line of text from the document, but it&rsquo;s part of a list. You can see the parent relationship back to <code>#/groups/0</code>. This text has a label of <code>list_item</code> which indicates that it&rsquo;s part of a list. There&rsquo;s also a <code>marker</code> field so you can extract the bullet point character if you want to.</p> <p>What does this look like in the original document?</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="826" height="394" class="mx-auto my-0 rounded-md" alt="list-in-document.png" loading="lazy" decoding="async" src="https://major.io/p/fun-with-docling/list-in-document_hu_52442e22d188c6fe.png" srcset="https://major.io/p/fun-with-docling/list-in-document_hu_b53c8f61b2819c02.png 330w,https://major.io/p/fun-with-docling/list-in-document_hu_52442e22d188c6fe.png 660w ,https://major.io/p/fun-with-docling/list-in-document.png 826w ,https://major.io/p/fun-with-docling/list-in-document.png 826w " sizes="100vw" /> </picture> </figure> </p> <p>So if you found a piece of text that interest you, you can walk backwards to the group and then higher in the document if needed.</p> <h1 id="wandering-around-documents" class="relative group">Wandering around documents <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#wandering-around-documents" aria-label="Anchor">#</a></span></h1><p>Let&rsquo;s break out some Python and see what we can do with documents.</p> <h2 id="getting-child-items" class="relative group">Getting child items <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#getting-child-items" aria-label="Anchor">#</a></span></h2><p>First off, let&rsquo;s find that <code>&quot;#/groups/0&quot;</code> from earlier and get the texts inside of it:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">docling_core.types.doc.document</span> <span class="kn">import</span> <span class="n">DoclingDocument</span> </span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="n">DoclingDocument</span><span class="o">.</span><span class="n">load_from_json</span><span class="p">(</span><span class="s2">&#34;enhancing_ohlc_data.json&#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"># Extract group 0 and find its children.</span> </span></span><span class="line"><span class="cl"><span class="n">my_group</span> <span class="o">=</span> <span class="n">doc</span><span class="o">.</span><span class="n">groups</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="n">refs</span> <span class="o">=</span> <span class="n">my_group</span><span class="o">.</span><span class="n">children</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Loop through the children,</span> </span></span><span class="line"><span class="cl"><span class="c1"># resolve their location,</span> </span></span><span class="line"><span class="cl"><span class="c1"># and print the original text.</span> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">refs</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">x</span><span class="o">.</span><span class="n">resolve</span><span class="p">(</span><span class="n">doc</span><span class="o">=</span><span class="n">doc</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nb">print</span><span class="p">(</span><span class="n">text</span><span class="o">.</span><span class="n">orig</span><span class="p">)</span> </span></span></code></pre></div><p>After running this, I get the text from the bulleted list:</p> <pre tabindex="0"><code>ยท Market relevance : VWAP is widely followed by institutional investors, trading algorithms, and benchmark providers [6] [2]. As such, it is a meaningful and valuable quantity to predict. ยท Interval-wide robustness : Unlike point-in-time prices such as the open or close, VWAP summarizes trading activity across the full interval, making it a more stable and representative measure of price. ยท Reduced discretization noise : VWAP is a continuous, volume-weighted price and is therefore less affected by the rounding effects of penny-level price changes. In contrast, returns computed from last-trade prices (e.g., close-to-close) are often either exactly zero or artificially large due to the $0.01 minimum tick increment. ยท Weaker arbitrage incentives : Predicting the direction of VWAP changes does not lend itself to straightforward arbitrage. A trader who knows that the next bar&#39;s VWAP will be higher than the current one cannot directly profit from this knowledge unless they had already executed near the current VWAP. As a result, patterns in VWAP returns may persist even in broadly efficient markets. </code></pre><h2 id="getting-parent-items" class="relative group">Getting parent items <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#getting-parent-items" aria-label="Anchor">#</a></span></h2><p>What if we wanted to go the other way around? Perhaps there was something really good in <code>#/texts/55</code> that we wanted to find in the document.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">docling_core.types.doc.document</span> <span class="kn">import</span> <span class="n">DoclingDocument</span> </span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="n">DoclingDocument</span><span class="o">.</span><span class="n">load_from_json</span><span class="p">(</span><span class="s2">&#34;enhancing_ohlc_data.json&#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"># Extract text 55 and get the parent</span> </span></span><span class="line"><span class="cl"><span class="n">my_text</span> <span class="o">=</span> <span class="n">doc</span><span class="o">.</span><span class="n">texts</span><span class="p">[</span><span class="mi">55</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="n">parent_ref</span> <span class="o">=</span> <span class="n">my_text</span><span class="o">.</span><span class="n">parent</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Resolve the parent reference</span> </span></span><span class="line"><span class="cl"><span class="n">parent_item</span> <span class="o">=</span> <span class="n">parent_ref</span><span class="o">.</span><span class="n">resolve</span><span class="p">(</span><span class="n">doc</span><span class="o">=</span><span class="n">doc</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">parent_item</span><span class="p">)</span> </span></span></code></pre></div><p>Run this and we get the details of <code>#/groups/0</code>:</p> <pre tabindex="0"><code>ListGroup( self_ref=&#39;#/groups/0&#39;, parent=RefItem(cref=&#39;#/body&#39;), children=[RefItem(cref=&#39;#/texts/53&#39;), RefItem(cref=&#39;#/texts/54&#39;), RefItem(cref=&#39;#/texts/55&#39;), RefItem(cref=&#39;#/texts/56&#39;)], content_layer=&lt;ContentLayer.BODY: &#39;body&#39;&gt;, name=&#39;list&#39;, label=&lt;GroupLabel.LIST: &#39;list&#39;&gt; ) </code></pre><h2 id="removing-items" class="relative group">Removing items <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#removing-items" aria-label="Anchor">#</a></span></h2><p>You can also remove individual items from a document or certain classes of items. Let&rsquo;s assume that you don&rsquo;t want any lists in your document. You can remove them like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">docling_core.types.doc.document</span> <span class="kn">import</span> <span class="n">DoclingDocument</span><span class="p">,</span> <span class="n">ListItem</span> </span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="n">DoclingDocument</span><span class="o">.</span><span class="n">load_from_json</span><span class="p">(</span><span class="s2">&#34;enhancing_ohlc_data.json&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">total_texts</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">doc</span><span class="o">.</span><span class="n">texts</span><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;Total texts in document: </span><span class="si">{</span><span class="n">total_texts</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">list_items</span> <span class="o">=</span> <span class="p">[</span><span class="n">x</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">doc</span><span class="o">.</span><span class="n">texts</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">ListItem</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">delete_items</span><span class="p">(</span><span class="n">node_items</span><span class="o">=</span><span class="n">list_items</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">total_texts_after_deletion</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">doc</span><span class="o">.</span><span class="n">texts</span><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;Total texts after deletion: </span><span class="si">{</span><span class="n">total_texts_after_deletion</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span> </span></span></code></pre></div><p>Running this shows that the list items were removed:</p> <pre tabindex="0"><code>&gt; uv run python src/doclingfun/main.py Total texts in document: 338 Total texts after deletion: 324 </code></pre><h2 id="converting-to-other-formats" class="relative group">Converting to other formats <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#converting-to-other-formats" aria-label="Anchor">#</a></span></h2><p>Finally, you can convert documents to other formats. You can certainly do something simple like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">doc</span><span class="o">.</span><span class="n">save_as_markdown</span><span class="p">(</span><span class="s2">&#34;enhancing_ohlc_data_modified.md&#34;</span><span class="p">)</span> </span></span></code></pre></div><p>But what if we wanted our group from earlier serialized to markdown without any other document contents. You can!</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">docling_core.transforms.serializer.markdown</span> <span class="kn">import</span> <span class="n">MarkdownDocSerializer</span> </span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">docling_core.types.doc.document</span> <span class="kn">import</span> <span class="n">DoclingDocument</span> </span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="n">DoclingDocument</span><span class="o">.</span><span class="n">load_from_json</span><span class="p">(</span><span class="s2">&#34;enhancing_ohlc_data.json&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">my_group</span> <span class="o">=</span> <span class="n">doc</span><span class="o">.</span><span class="n">groups</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="n">serializer</span> <span class="o">=</span> <span class="n">MarkdownDocSerializer</span><span class="p">(</span><span class="n">doc</span><span class="o">=</span><span class="n">doc</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="n">ser_res</span> <span class="o">=</span> <span class="n">serializer</span><span class="o">.</span><span class="n">serialize</span><span class="p">(</span><span class="n">item</span><span class="o">=</span><span class="n">my_group</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">ser_res</span><span class="o">.</span><span class="n">text</span><span class="p">)</span> </span></span></code></pre></div><p>That results in the same text as before, but now the bullets are removed and replaced with Markdown-style dashes:</p> <pre tabindex="0"><code>- Market relevance : VWAP is widely followed by institutional investors, trading algorithms, and benchmark providers [6] [2]. As such, it is a meaningful and valuable quantity to predict. - Interval-wide robustness : Unlike point-in-time prices such as the open or close, VWAP summarizes trading activity across the full interval, making it a more stable and representative measure of price. - Reduced discretization noise : VWAP is a continuous, volume-weighted price and is therefore less affected by the rounding effects of penny-level price changes. In contrast, returns computed from last-trade prices (e.g., close-to-close) are often either exactly zero or artificially large due to the $0.01 minimum tick increment. - Weaker arbitrage incentives : Predicting the direction of VWAP changes does not lend itself to straightforward arbitrage. A trader who knows that the next bar&#39;s VWAP will be higher than the current one cannot directly profit from this knowledge unless they had already executed near the current VWAP. As a result, patterns in VWAP returns may persist even in broadly efficient markets. </code></pre><h1 id="more-to-explore" class="relative group">More to explore <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#more-to-explore" aria-label="Anchor">#</a></span></h1><p>Docling has <em>lots more features</em> that I haven&rsquo;t covered here. For example, it can do <a href="https://docling-project.github.io/docling/examples/tesseract_lang_detection/" target="_blank" rel="noreferrer">OCR on images</a>. You can also <a href="https://docling-project.github.io/docling/examples/minimal_vlm_pipeline/" target="_blank" rel="noreferrer">use LLMs to help with parsing</a>. It also comes with a great <a href="https://docling-project.github.io/docling/examples/hybrid_chunking/#basic-usage" target="_blank" rel="noreferrer">hybrid chunker</a> to break documents into smaller pieces for embedding into a RAG database.</p>Getting podman quadlets talking to each otherhttps://major.io/p/quadlet-networking/Thu, 25 Sep 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/quadlet-networking/<p>Quadlets are a handy way to manage containers using systemd unit files. Containers running via quadlets have access to the external network by default, but they don&rsquo;t automatically communicate with each other like they do in a <code>docker-compose</code> setup. Adding networking only requires a few extra steps.</p> <h2 id="setting-up-some-quadlets" class="relative group">Setting up some quadlets <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#setting-up-some-quadlets" aria-label="Anchor">#</a></span></h2><p>I often need a postgres server laying around on my local machine for quick tasks or testing something I&rsquo;m working on. Lately, I&rsquo;ve been focused on RAG databases and that usually involves <a href="https://github.com/pgvector/pgvector" target="_blank" rel="noreferrer">pgvector</a>.</p> <p>The pgvector extension adds vector data types and functions to PostgreSQL, which is great for storing embeddings from machine learning models. You can search via all of the usual SQL queries that you&rsquo;re used to, but pgvector adds new capabilities for searching rows based on vector similarity.</p> <p>Here&rsquo;s the quadlet for pgvector in <code>~/.config/containers/systemd/pgvector.container</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><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">pgvector container</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">[Container]</span> </span></span><span class="line"><span class="cl"><span class="na">Image</span><span class="o">=</span><span class="s">docker.io/pgvector/pgvector:pg17</span> </span></span><span class="line"><span class="cl"><span class="na">Volume</span><span class="o">=</span><span class="s">pgvector:/var/lib/postgresql/data</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">POSTGRES_USER=postgres</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">POSTGRES_PASSWORD=secrete</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">POSTGRES_DB=postgres</span> </span></span><span class="line"><span class="cl"><span class="na">PublishPort</span><span class="o">=</span><span class="s">5432:5432</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">Restart</span><span class="o">=</span><span class="s">unless-stopped</span> </span></span></code></pre></div><p>This gets a postgres server with pgvector up and running with a persistent volume. It&rsquo;s listening on the default port 5432.</p> <p>Sometimes I&rsquo;m in a hurry and <a href="https://www.pgadmin.org/" target="_blank" rel="noreferrer">pgadmin4</a> is a quick way to poke around the database. It&rsquo;s also a good example here since it needs to talk to the pgvector container. Here&rsquo;s the quadlet for pgadmin4 in <code>~/.config/containers/systemd/pgadmin4.container</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><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">pgAdmin4 container</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">[Container]</span> </span></span><span class="line"><span class="cl"><span class="na">Image</span><span class="o">=</span><span class="s">docker.io/dpage/pgadmin4:latest</span> </span></span><span class="line"><span class="cl"><span class="na">Volume</span><span class="o">=</span><span class="s">pgadmin4:/var/lib/pgadmin</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_DEFAULT_EMAIL=major@mhtx.net</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_DEFAULT_PASSWORD=secrete</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_CONFIG_SERVER_MODE=False</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False</span> </span></span><span class="line"><span class="cl"><span class="na">PublishPort</span><span class="o">=</span><span class="s">8080:80</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">Restart</span><span class="o">=</span><span class="s">unless-stopped</span> </span></span></code></pre></div><p>Awesome! Let&rsquo;s reload the systemd configuration for my user account and start these containers:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user daemon-reload </span></span><span class="line"><span class="cl">systemctl --user start pgvector </span></span><span class="line"><span class="cl">systemctl --user start pgadmin4 </span></span></code></pre></div><p>We can check the running containers:</p> <pre tabindex="0"><code>&gt; podman ps --format &#34;table {{.ID}}\t{{.Names}}&#34; CONTAINER ID NAMES b099fdaa6b18 valkey f8ab764c299c systemd-pgadmin4 052c160fb45b systemd-pgvector </code></pre><h2 id="testing-communication" class="relative group">Testing communication <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#testing-communication" aria-label="Anchor">#</a></span></h2><p>Let&rsquo;s hop into the pgadmin4 container and see if we can connect to the pgvector database:</p> <pre tabindex="0"><code>&gt; podman exec -it systemd-pgadmin4 ping pgvector -c 4 ping: bad address &#39;pgvector&#39; &gt; podman exec -it systemd-pgadmin4 ping systemd-pgvector -c 4 ping: bad address &#39;systemd-pgvector&#39; </code></pre><p>This isn&rsquo;t great. There are two problems here:</p> <ol> <li>The containers aren&rsquo;t on the same network</li> <li>I want to refer to the pgvector container as <code>pgvector</code>, not <code>systemd-pgvector</code></li> </ol> <p>Let&rsquo;s fix that.</p> <h2 id="fixing-communication" class="relative group">Fixing communication <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#fixing-communication" aria-label="Anchor">#</a></span></h2><p>Open up the <code>~/.config/containers/systemd/pgvector.container</code> file and make the two changes noted below with comments:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><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">pgvector container</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">[Container]</span> </span></span><span class="line"><span class="cl"><span class="c1"># Use a consistent name ๐Ÿ‘‡</span> </span></span><span class="line"><span class="cl"><span class="na">ContainerName</span><span class="o">=</span><span class="s">pgvector</span> </span></span><span class="line"><span class="cl"><span class="na">Image</span><span class="o">=</span><span class="s">docker.io/pgvector/pgvector:pg17</span> </span></span><span class="line"><span class="cl"><span class="na">Volume</span><span class="o">=</span><span class="s">pgvector:/var/lib/postgresql/data</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">POSTGRES_USER=postgres</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">POSTGRES_PASSWORD=secrete</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">POSTGRES_DB=postgres</span> </span></span><span class="line"><span class="cl"><span class="c1"># Add the container to a network ๐Ÿ‘‡</span> </span></span><span class="line"><span class="cl"><span class="na">Network</span><span class="o">=</span><span class="s">db-network</span> </span></span><span class="line"><span class="cl"><span class="na">PublishPort</span><span class="o">=</span><span class="s">5432:5432</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">Restart</span><span class="o">=</span><span class="s">unless-stopped</span> </span></span></code></pre></div><p>Also open the <code>~/.config/containers/systemd/pgadmin4.container</code> file and make the same network change:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><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">pgAdmin4 container</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">[Container]</span> </span></span><span class="line"><span class="cl"><span class="na">Image</span><span class="o">=</span><span class="s">docker.io/dpage/pgadmin4:latest</span> </span></span><span class="line"><span class="cl"><span class="na">Volume</span><span class="o">=</span><span class="s">pgadmin4:/var/lib/pgadmin</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_DEFAULT_EMAIL=major@mhtx.net</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_DEFAULT_PASSWORD=secrete</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_CONFIG_SERVER_MODE=False</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False</span> </span></span><span class="line"><span class="cl"><span class="c1"># Add the container to a network ๐Ÿ‘‡</span> </span></span><span class="line"><span class="cl"><span class="na">Network</span><span class="o">=</span><span class="s">db-network</span> </span></span><span class="line"><span class="cl"><span class="na">PublishPort</span><span class="o">=</span><span class="s">8080:80</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">Restart</span><span class="o">=</span><span class="s">unless-stopped</span> </span></span></code></pre></div><p>Create the network:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">podman network create db-network </span></span></code></pre></div><p>Now, reload the systemd configuration and restart the containers:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user daemon-reload </span></span><span class="line"><span class="cl">systemctl --user restart pgvector </span></span><span class="line"><span class="cl">systemctl --user restart pgadmin4 </span></span></code></pre></div><h2 id="testing-communication-again" class="relative group">Testing communication again <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#testing-communication-again" aria-label="Anchor">#</a></span></h2><p>Now, let&rsquo;s hop into the pgadmin4 container and see if we can connect to the pgvector database:</p> <pre tabindex="0"><code>&gt; podman exec -it systemd-pgadmin4 ping pgvector -c 4 PING pgvector (10.89.5.6): 56 data bytes 64 bytes from 10.89.5.6: seq=0 ttl=42 time=0.026 ms 64 bytes from 10.89.5.6: seq=1 ttl=42 time=0.036 ms 64 bytes from 10.89.5.6: seq=2 ttl=42 time=0.034 ms 64 bytes from 10.89.5.6: seq=3 ttl=42 time=0.088 ms --- pgvector ping statistics --- 4 packets transmitted, 4 packets received, 0% packet loss round-trip min/avg/max = 0.026/0.046/0.088 ms </code></pre><p><strong>Perfect!</strong> ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰</p> <h2 id="extra-credit" class="relative group">Extra credit <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#extra-credit" aria-label="Anchor">#</a></span></h2><p>If you want to deploy your system with automation and avoid the manual network creation, you can add one extra file to your <code>~/.config/containers/systemd/</code> directory. Save this as <code>db-network.network</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Network]</span> </span></span><span class="line"><span class="cl"><span class="na">Label</span><span class="o">=</span><span class="s">app=db-network</span> </span></span></code></pre></div>Monitor system and GPU performance with Performance Co-Pilothttps://major.io/p/performance-copilot-fedora-gpu/Tue, 23 Sep 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/performance-copilot-fedora-gpu/<p>I&rsquo;ve used so many performance monitoring tools and systems over the years. When you need to know information right now, tools like <a href="https://github.com/aristocratos/btop" target="_blank" rel="noreferrer">btop</a> and <a href="https://nicolargo.github.io/glances/" target="_blank" rel="noreferrer">glances</a> are great for quick overviews. Historical data is fairly easy to pick through with <a href="https://github.com/sysstat/sysstat" target="_blank" rel="noreferrer">sysstat</a>.</p> <p>However, when you want a comprehensive view of system performance over time, especially with GPU metrics for machine learning workloads, <a href="https://pcp.io/" target="_blank" rel="noreferrer">Performance Co-Pilot (PCP)</a> is an excellent choice. It has some handy integrations with <a href="https://cockpit-project.org/" target="_blank" rel="noreferrer">Cockpit</a> for web-based monitoring, but I prefer using the command line tools directly.</p> <p>This post explains how to set up PCP on Fedora and enable some very basic GPU monitoring for both NVIDIA and AMD GPUs.</p> <h2 id="installing-performance-co-pilot" class="relative group">Installing Performance Co-Pilot <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#installing-performance-co-pilot" aria-label="Anchor">#</a></span></h2><p>Install the core packages and command line tools:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf install pcp pcp-system-tools </span></span></code></pre></div><p>Enable and start the PCP services:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now pmcd pmlogger </span></span><span class="line"><span class="cl">sudo systemctl status pmcd </span></span></code></pre></div><p>These two services work together like a team:</p> <ul> <li><code>pmcd</code> (Performance Metrics Collection Daemon) gathers real-time metrics from various sources on your system when you request them.</li> <li><code>pmlogger</code> records these metrics to log files for historical analysis.</li> </ul> <p>You can verify that the services are working as expected:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check available metrics</span> </span></span><span class="line"><span class="cl">pminfo <span class="p">|</span> head -20 </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># View current CPU utilization</span> </span></span><span class="line"><span class="cl">pmval kernel.all.cpu.user </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Show memory statistics</span> </span></span><span class="line"><span class="cl">pmstat -s <span class="m">5</span> </span></span></code></pre></div><h2 id="adding-gpu-metrics-collection" class="relative group">Adding GPU metrics collection <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#adding-gpu-metrics-collection" aria-label="Anchor">#</a></span></h2><p>I do a lot of LLM work locally and I&rsquo;d like to keep track of my GPU usage over time. Fortunately, PCP supports popular GPUs through something called a PMDA (Performance Metrics Domain Agent). These are packaged in Fedora, but they have an interesting installation process.</p> <h3 id="nvidia-gpus" class="relative group">NVIDIA GPUs <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#nvidia-gpus" aria-label="Anchor">#</a></span></h3><div class="flex rounded-md bg-primary-100 px-4 py-3 dark:bg-primary-900"> <span class="pe-3 text-primary-400"> <span class="icon relative inline-block px-1 align-text-bottom"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg> </span> </span> <span class="dark:text-neutral-300"><strong>Unverified instructions:</strong> I only have an AMD GPU, but I pulled this NVIDIA information from various places on the internet. Please let me know if you find any issues and I&rsquo;ll update the post!</span> </div> <p>For NVIDIA GPUs, ensure you have the NVIDIA drivers and <code>nvidia-ml</code> library:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check if nvidia-smi works</span> </span></span><span class="line"><span class="cl">nvidia-smi </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Install the NVIDIA management library if needed</span> </span></span><span class="line"><span class="cl">sudo dnf install nvidia-driver-cuda-libs </span></span></code></pre></div><p>Now install the NVIDIA PMDA:</p> <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> /var/lib/pcp/pmdas/nvidia </span></span><span class="line"><span class="cl">sudo ./Install </span></span></code></pre></div><p>The installer will prompt you for configuration options. Accept the defaults unless you have specific requirements.</p> <div class="flex rounded-md bg-primary-100 px-4 py-3 dark:bg-primary-900"> <span class="pe-3 text-primary-400"> <span class="icon relative inline-block px-1 align-text-bottom"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/></svg> </span> </span> <span class="dark:text-neutral-300">Thanks to Will Cohen for helping me get these NVIDIA steps corrected! ๐Ÿ‘</span> </div> <p>After installation, verify GPU metrics are available:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># List all NVIDIA metrics</span> </span></span><span class="line"><span class="cl">pminfo nvidia </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Check GPU utilization</span> </span></span><span class="line"><span class="cl">pmval nvidia.gpuactive </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Monitor GPU memory usage</span> </span></span><span class="line"><span class="cl">pmval nvidia.memused </span></span></code></pre></div><h3 id="amd-gpus" class="relative group">AMD GPUs <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#amd-gpus" aria-label="Anchor">#</a></span></h3><p>For AMD GPUs, PCP provides the <code>amdgpu</code> PMDA that works with the ROCm stack:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Ensure rocm-smi is installed and working</span> </span></span><span class="line"><span class="cl">rocm-smi </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Install the AMD GPU PMDA package</span> </span></span><span class="line"><span class="cl">sudo dnf install pcp-pmda-amdgpu </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Install the PMDA</span> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> /var/lib/pcp/pmdas/amdgpu </span></span><span class="line"><span class="cl">sudo ./Install </span></span></code></pre></div><p>After installation, verify AMD GPU metrics:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># List all AMD GPU metrics</span> </span></span><span class="line"><span class="cl">pminfo amdgpu </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Check GPU utilization</span> </span></span><span class="line"><span class="cl">pmval amdgpu.gpu.load </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Monitor GPU memory usage</span> </span></span><span class="line"><span class="cl">pmval amdgpu.memory.used </span></span></code></pre></div><h2 id="querying-performance-data" class="relative group">Querying performance data <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#querying-performance-data" aria-label="Anchor">#</a></span></h2><p>There are lots of handy tools for querying PCP data depending on whether you need information about something happening now or want to analyze historical trends.</p> <h3 id="real-time-monitoring-with-pmrep" class="relative group">Real-time monitoring with pmrep <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#real-time-monitoring-with-pmrep" aria-label="Anchor">#</a></span></h3><p>The <code>pmrep</code> tool provides formatted output perfect for dashboards or scripts. It&rsquo;s great for situations where you need to see what&rsquo;s happening right now. It&rsquo;s much like <code>iostat</code> or <code>vmstat</code> from the sysstat package, but you get a lot more flexibility.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># System overview with 1-second updates</span> </span></span><span class="line"><span class="cl">pmrep --space-scale<span class="o">=</span>MB -t <span class="m">1</span> kernel.all.load kernel.all.cpu.user mem.util.used </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># GPU metrics for LLM monitoring (NVIDIA)</span> </span></span><span class="line"><span class="cl">pmrep --space-scale<span class="o">=</span>MB -t1 nvidia.gpuactive nvidia.memused nvidia.temperature </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># GPU metrics for LLM monitoring (AMD)</span> </span></span><span class="line"><span class="cl">pmrep --space-scale<span class="o">=</span>MB -t <span class="m">1</span> amdgpu.gpu.load amdgpu.memory.used amdgpu.gpu.temperature </span></span></code></pre></div><h3 id="historical-analysis-with-pmlogsummary" class="relative group">Historical analysis with pmlogsummary <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#historical-analysis-with-pmlogsummary" aria-label="Anchor">#</a></span></h3><p>If you&rsquo;re used to to running <code>sar</code> commands from the sysstat package, you&rsquo;ll find <code>pmlogsummary</code> very familiar. Again, you can do a lot more with <code>pmlogsummary</code> than with <code>sar</code>, but the basic concepts are similar.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Summarize yesterday&#39;s GPU utilization (NVIDIA)</span> </span></span><span class="line"><span class="cl">pmlogsummary -S @yesterday -T @today /var/log/pcp/pmlogger/<span class="k">$(</span>hostname<span class="k">)</span>/<span class="k">$(</span>date -d yesterday +%Y%m%d<span class="k">)</span> nvidia.gpuactive </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Summarize yesterday&#39;s GPU utilization (AMD)</span> </span></span><span class="line"><span class="cl">pmlogsummary -S @yesterday -T @today /var/log/pcp/pmlogger/<span class="k">$(</span>hostname<span class="k">)</span>/<span class="k">$(</span>date -d yesterday +%Y%m%d<span class="k">)</span> amdgpu.gpu.load </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Find peak memory usage over the last hour</span> </span></span><span class="line"><span class="cl">pmlogsummary -S -1hour /var/log/pcp/pmlogger/<span class="k">$(</span>hostname<span class="k">)</span>/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span> mem.util.used </span></span></code></pre></div><h2 id="troubleshooting-tips" class="relative group">Troubleshooting tips <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#troubleshooting-tips" aria-label="Anchor">#</a></span></h2><p>If GPU metrics aren&rsquo;t showing up:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check if the PMDA is properly installed</span> </span></span><span class="line"><span class="cl">pminfo -f pmcd.agent <span class="p">|</span> grep -E <span class="s2">&#34;amdgpu|nvidia&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Restart PMCD to reload PMDAs</span> </span></span><span class="line"><span class="cl">sudo systemctl restart pmcd </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Check PMDA logs for errors</span> </span></span><span class="line"><span class="cl">sudo journalctl -u pmcd -n <span class="m">50</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Verify GPU drivers are working</span> </span></span><span class="line"><span class="cl">rocm-smi <span class="c1"># for AMD</span> </span></span><span class="line"><span class="cl">nvidia-smi <span class="c1"># for NVIDIA</span> </span></span></code></pre></div><h2 id="further-reading" class="relative group">Further reading <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#further-reading" aria-label="Anchor">#</a></span></h2><ul> <li><a href="https://pcp.io/documentation.html" target="_blank" rel="noreferrer">Performance Co-Pilot documentation</a> - Official PCP documentation and quick reference guides</li> <li><a href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/monitoring_and_managing_system_status_and_performance/monitoring-performance-with-performance-co-pilot_monitoring-and-managing-system-status-and-performance" target="_blank" rel="noreferrer">Red Hat&rsquo;s PCP guide</a> - Enterprise deployment patterns and best practices</li> <li><a href="https://pcp.readthedocs.io/en/latest/PG/PMAPI.html" target="_blank" rel="noreferrer">PMAPI</a> - Performance metrics API</li> </ul>Summarize YouTube videos with Fabrichttps://major.io/p/summarize-youtube-videos-fabric/Mon, 22 Sep 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/summarize-youtube-videos-fabric/<p>I watch plenty of YouTube videos with instructional content. Some are related to my work while many others involve my hobbies, such as ham radio and financial markets. Lots of them have really useful information in them, but I struggle with taking notes while I&rsquo;m watching.</p> <p>It turns out there&rsquo;s a tool for that! During some face to face work meetings last week, a coworker showed off <a href="https://github.com/danielmiessler/Fabric" target="_blank" rel="noreferrer">fabric</a>.</p> <p>Let&rsquo;s go through how to use it with some examples.</p> <h2 id="installation--setup" class="relative group">Installation &amp; setup <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#installation--setup" aria-label="Anchor">#</a></span></h2><p>The author, Daniel Miessler, offers lots of installation methods in the <a href="https://github.com/danielmiessler/Fabric?tab=readme-ov-file#installation" target="_blank" rel="noreferrer">README</a>. Fedora users will also need <code>yt-dlp</code> to download YouTube videos:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf install yt-dlp </span></span></code></pre></div><p>Next, run <code>fabric setup</code> to enter the configuration menu:</p> <pre tabindex="0"><code>&gt; fabric --setup Available plugins (please configure all required plugins):: AI Vendors [at least one, required] [1] AIML [2] Anthropic (configured) [3] Azure [4] Cerebras [5] DeepSeek [6] Exolab [7] Gemini [8] GrokAI [9] Groq [10] Langdock [11] LiteLLM [12] LM Studio [13] Mistral [14] Ollama [15] OpenAI [16] OpenRouter [17] Perplexity [18] SiliconCloud [19] Together [20] Venice AI Tools [21] Custom Patterns - Set directory for your custom patterns (optional) [22] Default AI Vendor and Model [required] (configured) [23] Jina AI Service - to grab a webpage as clean, LLM-friendly text (configured) [24] Language - Default AI Vendor Output Language (configured) [25] Patterns - Downloads patterns [required] (configured) [26] Strategies - Downloads Prompting Strategies (like chain of thought) [required] [27] YouTube - to grab video transcripts (via yt-dlp) and comments/metadata (via YouTube API) (configured) [Plugin Number] Enter the number of the plugin to setup (leave empty to skip): </code></pre><p>In my case, I did the following:</p> <ul> <li>Option 2: Anthropic (Claude Opus)</li> <li>Option 21: Download the patterns (prompts for the LLM)</li> <li>Option 22: Set Anthropic as the default AI vendor</li> <li>Option 27: YouTube setup with an <a href="https://developers.google.com/youtube/v3/getting-started" target="_blank" rel="noreferrer">API key from Google</a></li> </ul> <p>Now it&rsquo;s time to summarize some videos!</p> <h2 id="basic-summarization" class="relative group">Basic summarization <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#basic-summarization" aria-label="Anchor">#</a></span></h2><p>There are <a href="https://github.com/danielmiessler/Fabric/tree/main/data/patterns" target="_blank" rel="noreferrer">lots of patterns</a> in the repository to choose from, but let&rsquo;s just use the <code>summarize</code> pattern for now. One of my favorite people on YouTube for information on financial markets is <a href="https://www.youtube.com/user/CiovaccoCapital" target="_blank" rel="noreferrer">Chris Ciovacco</a>. He publishes videos weekly on Fridays with lots of inter-market analysis and some helpful trading psychology reminders.</p> <p>His most recent video has this URL:</p> <pre tabindex="0"><code>https://www.youtube.com/watch?v=eFiLaRNw8XM </code></pre><p>We can use the video URL with fabric to get a summary:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">&gt; fabric -y <span class="s2">&#34;https://youtube.com/watch?v=eFiLaRNw8XM&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --stream --pattern summarize </span></span></code></pre></div><p>And the summary:</p> <pre tabindex="0"><code>ONE SENTENCE SUMMARY: Market analysis shows strong uptrend intact with multiple bullish signals supporting potential continued gains through 2029-2033. MAIN POINTS: 1. Fed cut rates 25 basis points with additional cuts likely in pipeline 2. Market could fall 3-14% without damaging existing uptrend from April low 3. Fed cuts within 1% of highs historically led to gains 16/16 times 4. Dow stocks breadth thrust signal triggered, historically very bullish long-term indicator 5. NASDAQ breadth signals show consistent positive returns over 1-5 year periods 6. Multiple technical indicators suggest keeping open mind about strong outcomes 7. S&amp;P 500 RSI above 45 for 102 days indicates short-term overbought conditions 8. Bank of America predicts secular bull market lasting until 2029-2033 9. Current uptrend remains intact based on volume-weighted average price levels 10. Weight of evidence approach favors patience over frequent trading decisions TAKEAWAYS: 1. Stay patient during normal 3-14% pullbacks as they won&#39;t damage the uptrend structure 2. Multiple rare breadth thrust signals historically produced excellent 1-5 year returns 3. Fed rate cuts near market highs have perfect historical track record of gains 4. Short-term overbought conditions may cause 2% drop but shouldn&#39;t affect long-term strategy 5. Secular bull market could extend to 2029-2033 based on historical data patterns </code></pre><p>This is accurate and it gives me some good tidbits of information as I think about placing trades for the next week.</p> <p>What actually happened here?</p> <ol> <li>Fabric downloaded the transcript of the video using <code>yt-dlp</code>.</li> <li>It sent the transcript to Claude Opus with a prompt asking for a summary.</li> <li>Claude Opus returned the summary, which fabric printed to the terminal.</li> </ol> <h2 id="extracting-wisdom" class="relative group">Extracting wisdom <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#extracting-wisdom" aria-label="Anchor">#</a></span></h2><p>There are so many patterns that come with fabric, but one of my other favorites is <code>extract_wisdom</code>. Here&rsquo;s the same video as before, but now with the new pattern:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">&gt; fabric -y <span class="s2">&#34;https://youtube.com/watch?v=eFiLaRNw8XM&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --stream --pattern extract_wisdom </span></span></code></pre></div><p>There&rsquo;s much, much more detail here:</p> <pre tabindex="0"><code># SUMMARY Stock market analyst reviews Fed rate cuts, technical analysis, and historical data suggesting strong upside potential through 2029-2035 secular bull market. # IDEAS - Fed cut rates 25 basis points with additional cuts likely in pipeline ahead - Market could fall 3-14% without damaging existing uptrend from April low significantly - When Fed cuts within 1% of S&amp;P highs, market higher year later 16/16 times - Average gain nearly 15% when Fed cuts near all-time market highs historically - Dow stocks breadth thrust above 85% after oversold readings signals strong performance ahead - After similar breadth signals, S&amp;P 500 higher one year later in every instance recorded - Median gain 16.2% one year after Dow breadth thrust signals trigger historically - Three-year returns average 41.45% after rare Dow breadth thrust signals trigger successfully - Five-year returns average 82% after Dow breadth thrust signals with 76% median gain - NASDAQ breadth thrust signals just triggered in September 2025 for first time recently - After NASDAQ breadth signals, one-year median gains reach 18% with 17% average historically - Three years after NASDAQ signals, every case shows gains with 48% average return - Five years after NASDAQ signals, every case higher with 84% average gain recorded - NASDAQ 200-day breadth moves rare but produce 15% one-year gains on average - Two years after NASDAQ 200-day signals, market gains average over 34% consistently - Four years after NASDAQ signals, every case higher with 56% average and median - S&amp;P 500 RSI above 45 for 102 days represents one of longest streaks ever - After similar RSI streaks, market drops 2.17% average over following two weeks - Bank of America predicts secular bull market lasting until 2029 or 2033 timeframe - Secular bull market could extend to 2034-2035 based on historical data patterns - Midcap relative performance made new 52-week low despite bullish breadth signals recently - Midcap underperformance by 73% week-to-date following Fed meeting and rate cut - Volatility remains necessary evil for capturing satisfying long-term investment gains and compounding - Weight of evidence approach requires flexible, unbiased and open mind for decisions - Patient investors holding during uptrends benefit when markets make new higher highs # INSIGHTS - Historical Fed rate cuts near market peaks consistently predict positive one-year market performance - Breadth thrust signals across multiple indices suggest broad market participation and strength ahead - Short-term overbought conditions create minor pullbacks within larger secular bull market trends - Weight of evidence approach balances bullish signals with realistic volatility expectations for investors - Secular bull markets driven by demographics can extend much longer than typical expectations - Technical analysis provides objective framework for distinguishing volatility from trend damage assessment - Multiple independent signals converging suggests higher probability of sustained market uptrend continuation - Patient long-term investors benefit most from riding out normal volatility within uptrends - Historical data patterns repeat with remarkable consistency across different market cycle periods - Professional investment firms reach similar conclusions when analyzing same objective historical data sets # QUOTES - &#34;The market could backtrack quite a bit here. It wouldn&#39;t do a lot of damage to the existing uptrend.&#34; - &#34;When the Fed cuts rates within 1% of an S&amp;P 500 all-time high, they&#39;ve done so 16 times historically.&#34; - &#34;In every single case, 16 for 16, the S&amp;P 500 was higher a year later from the date of the cut.&#34; - &#34;It&#39;s very very easy to see green tables and believe that the market is never going to go down again.&#34; - &#34;Volatility is a necessary evil. The ability to ride out volatility is a necessary evil.&#34; - &#34;We&#39;re thinking in probabilities rather than certainties.&#34; - &#34;Bank of America came out recently and said that we&#39;re in a secular bull market that could last until 2029 or 2033.&#34; - &#34;The objective of all of this is to make money.&#34; - &#34;We don&#39;t want to get caught off guard if the market falls 2% over the next two weeks.&#34; - &#34;We want to continue to make decisions based on the weight of the evidence.&#34; - &#34;The only way that we can use the weight of the evidence effectively is if we head into next week and every week with that flexible, unbiased and open mind.&#34; # HABITS - Monitor health of existing market trends using multiple technical analysis reference points daily - Use anchored volume weighted average price lines to assess uptrend integrity continuously - Prepare mitigation strategies in advance for various market pullback scenarios and percentage levels - Focus on weight of evidence approach rather than single indicators for investment decisions - Maintain fully invested positions during confirmed uptrends to maximize long-term compound growth - Trade less rather than more in bull markets to minimize taxable events - Study historical precedents and patterns to maintain realistic expectations about market volatility - Review breadth data across multiple indices to assess broad market participation levels - Analyze relative performance between asset classes to identify leadership and lagging sectors - Maintain flexible, unbiased mindset when interpreting conflicting market signals and data points - Prepare mentally for normal volatility to avoid emotional decision-making during market stress - Use multiple timeframes from weeks to years when analyzing investment performance expectations - Document and track rare market signals to build database of historical precedents - Balance bullish signals with bearish data to maintain realistic market outlook perspectives - Consult multiple professional sources to validate independent analysis and investment thesis conclusions # FACTS - Fed has cut rates within 1% of S&amp;P highs 16 times with 100% success rate - S&amp;P 500 RSI stayed above 45 for 102 straight days, longest streak in history - After similar RSI streaks, market drops average 2.17% over following two weeks historically - Dow breadth thrust signals occurred only handful of times since 2001 market data available - NASDAQ breadth signals show 86% success rate one year later with 18% median gains - After NASDAQ 200-day signals, four-year performance shows 100% success rate with 56% gains - Midcap performance relative to S&amp;P 500 recently made new 52-week low this year - Bank of America predicts secular bull market lasting until 2029 or 2033 based on data - Dotcom bear market lasted approximately three years from 2000 peak to 2003 retest - S&amp;P 400 midcap new high/low data available going back only to 2010 currently - NASDAQ 100 breadth data extends back to 2002 for historical comparison analysis purposes - Current market uptrend began from April low based on technical analysis reference points - Fed meeting occurred Wednesday September 17th with 25 basis point rate cut announced - Wall Street Journal reported all major indices performed well following Fed rate cut decision - Professional investment analysis reaches similar 2034-2035 secular bull market target dates consistently # REFERENCES - JP Morgan historical data on Fed rate cuts near market highs - Wall Street Journal headlines following Fed meeting coverage - Bank of America secular bull market research and predictions - August 8th video covering annotated chart analysis - Last week&#39;s video on six recent stock market signals - S&amp;P 500, S&amp;P 400, NASDAQ 100, and Dow Jones index data - RSI (Relative Strength Index) technical indicator analysis - Anchored Volume Weighted Average Price (AWOP) technical analysis tool - 150-day and 200-day simple moving averages for breadth analysis - Christmas Eve market low historical reference point - COVID market crash and recovery period analysis - Dotcom bubble and bear market historical comparison - Financial crisis market low and recovery analysis - 2010, 2011, 2015, 2016 market corrections and recovery patterns # ONE-SENTENCE TAKEAWAY Multiple historical signals suggest secular bull market continues through 2029-2035 despite normal volatility. # RECOMMENDATIONS - Maintain fully invested positions during confirmed uptrends to maximize long-term compound growth potential - Use weight of evidence approach combining multiple signals rather than single indicator decisions - Prepare mitigation strategies in advance for 3-14% pullbacks without panicking or selling positions - Focus on one to five-year performance expectations rather than short-term two-week market movements - Study historical precedents to maintain realistic expectations about normal market volatility patterns ahead - Monitor breadth data across multiple indices to assess broad market participation and strength - Keep flexible, unbiased mindset when interpreting conflicting market signals and data points consistently - Trade less frequently in bull markets to minimize taxable events and maximize position holding - Use technical analysis reference points to distinguish between volatility and actual trend damage - Maintain patient approach during normal pullbacks within context of existing secular bull trends - Analyze relative performance between asset classes to identify leadership and lagging sector rotation - Document rare market signals to build historical database for future investment decision making - Balance bullish signals with bearish data to maintain realistic market outlook and expectations - Consult multiple professional sources to validate independent analysis and investment thesis development conclusions - Prepare mentally for normal volatility to avoid emotional decision-making during temporary market stress </code></pre><h2 id="getting-clarification" class="relative group">Getting clarification <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#getting-clarification" aria-label="Anchor">#</a></span></h2><p>You can take any piece of the summary and query the LLM for a further explanation. If you saved the output to a file, then you can send the output right back to the LLM using the <code>-a</code> option.</p> <p>Or, you can take a small piece and ask for more detail with a pipe:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt; echo &#34;How do I do this myself on TradingView? Analyze relative performance between asset classes to identify leadership and lagging sector rotation&#34; | fabric --stream </span></span><span class="line"><span class="cl">Here&#39;s how to analyze relative performance and sector rotation on TradingView: </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">## 1. **Set Up Multi-Asset Comparison Charts** </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">### Basic Relative Performance Setup: </span></span><span class="line"><span class="cl">1. Open a new chart </span></span><span class="line"><span class="cl">2. Click the &#34;Compare&#34; button (or press &#34;+&#34; next to symbol) </span></span><span class="line"><span class="cl">3. Add multiple assets you want to compare </span></span><span class="line"><span class="cl">4. Switch to &#34;Percentage&#34; scale (right-click Y-axis โ†’ Percentage) </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">### Key Asset Classes to Compare: </span></span><span class="line"><span class="cl">- **Equities**: SPY, QQQ, IWM, EFA, EEM </span></span><span class="line"><span class="cl">- **Bonds**: TLT, IEF, HYG, LQD </span></span><span class="line"><span class="cl">- **Commodities**: GLD, SLV, DBC, USO </span></span><span class="line"><span class="cl">- **Sectors**: XLF, XLK, XLE, XLV, XLI, XLU, etc. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">--- SNIP --- </span></span></code></pre></div>RAG talk recap from DevConf.US 2025https://major.io/p/devconf-rag/Sat, 20 Sep 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/devconf-rag/<p>Hello from DevConf.US in Boston, Massachusetts!</p> <p>I presented yesterday about the challenges of developing systems for retrieval-augmented generation (RAG) with large language models (LLMs). If you&rsquo;ve been reading tech blogs lately, or the <a href="https://www.reddit.com/r/rag/" target="_blank" rel="noreferrer">r/rag subreddit</a>, you might think that implementing RAG is as simple as tossing documents into a database and watching the magic happen. <em>(Spoiler alert: it&rsquo;s not.)</em></p> <p>I&rsquo;ll quickly recap the presentation in this post, but you can also download the <a href="slides.pdf">slides as a PDF</a> or watch the <a href="https://www.youtube.com/live/i2H6tOu4Jyw?feature=shared&amp;t=8709" target="_blank" rel="noreferrer">video on YouTube</a>.</p> <h2 id="what-rag-actually-is-and-isnt" class="relative group">What RAG actually is (and isn&rsquo;t) <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-rag-actually-is-and-isnt" aria-label="Anchor">#</a></span></h2><p>Using RAG with an LLM is a lot like taking an open-note exam. If you know the concepts really well, but you need to quickly reference specific details, an open-note exam isn&rsquo;t too bad. However, if you don&rsquo;t know the material at all, having notes won&rsquo;t help you much. This means that the LLM that you pair with RAG needs some relevant training in the domain you&rsquo;re working in.</p> <p>But the big question is how to increase your odds of successfully delivering coherent, complete, and correct answers when you&rsquo;re up against this challenge:</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="960" height="540" class="mx-auto my-0 rounded-md" alt="what-is-llm.png" loading="lazy" decoding="async" src="https://major.io/p/devconf-rag/what-is-llm_hu_23fd8f1c2a4b68e2.png" srcset="https://major.io/p/devconf-rag/what-is-llm_hu_26669c56b3a132c1.png 330w,https://major.io/p/devconf-rag/what-is-llm_hu_23fd8f1c2a4b68e2.png 660w ,https://major.io/p/devconf-rag/what-is-llm.png 960w ,https://major.io/p/devconf-rag/what-is-llm.png 960w " sizes="100vw" /> </picture> </figure> </p> <p>This is where RAG can improve your odds.</p> <h2 id="the-fellowship-of-the-rag" class="relative group">The Fellowship of the RAG <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-fellowship-of-the-rag" aria-label="Anchor">#</a></span></h2><p>Building a RAG system isn&rsquo;t a solo quest. You need a diverse team, each bringing their own perspectives:</p> <p>Your senior engineers worry about security and data compliance. They&rsquo;re asking the tough questions: Is sensitive data protected? Can users access only what they should? Has the data been tampered with?</p> <p>Junior developers get overwhelmed by the document chaos. Where are documents stored? In what formats? How often do they change? Are they even accurate?</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="960" height="540" class="mx-auto my-0 rounded-md" alt="junior-developers.png" loading="lazy" decoding="async" src="https://major.io/p/devconf-rag/junior-developers_hu_fe339e504b6d5771.png" srcset="https://major.io/p/devconf-rag/junior-developers_hu_c660836df54a93d8.png 330w,https://major.io/p/devconf-rag/junior-developers_hu_fe339e504b6d5771.png 660w ,https://major.io/p/devconf-rag/junior-developers.png 960w ,https://major.io/p/devconf-rag/junior-developers.png 960w " sizes="100vw" /> </picture> </figure> </p> <p>Quality engineers face a new paradigm. Traditional deterministic testing doesn&rsquo;t work with AI systems that might give different answers to the same question.</p> <p>And then there&rsquo;s the AI enthusiast who just read 20 HackerNews articles and wants to implement every new technique. (We all know one.)</p> <p>The lesson? Your fellowship matters more than your technology stack.</p> <h2 id="common-pitfalls-in-the-mines-of-moria" class="relative group">Common pitfalls in the Mines of Moria <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#common-pitfalls-in-the-mines-of-moria" aria-label="Anchor">#</a></span></h2><p>Just like the Fellowship&rsquo;s journey through the Mines, RAG implementation has its share of monsters lurking in the dark.</p> <p><strong>Document parsing is harder than it looks.</strong> That 10,000-page PDF full of Excel tables, charts, and multi-column text written 15 years ago by someone who no longer works there? Yeah, that&rsquo;s your Balrog. Tools like Docling can help, but sometimes you need to accept that certain documents should stay buried.</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="960" height="540" class="mx-auto my-0 rounded-md" alt="balrog.png" loading="lazy" decoding="async" src="https://major.io/p/devconf-rag/balrog_hu_f96953c45b25b58b.png" srcset="https://major.io/p/devconf-rag/balrog_hu_a23bf7182441e56b.png 330w,https://major.io/p/devconf-rag/balrog_hu_f96953c45b25b58b.png 660w ,https://major.io/p/devconf-rag/balrog.png 960w ,https://major.io/p/devconf-rag/balrog.png 960w " sizes="100vw" /> </picture> </figure> </p> <p><strong>Search strategy matters.</strong> You&rsquo;ll need to choose between keyword search (fast but misses semantic meaning), vector search (understands context but requires expensive embedding), hybrid approaches, or graph-based methods. Start simple โ€“ you can always evolve your approach.</p> <p><strong>Model size affects everything.</strong> Smaller models need more accurate RAG context because they have less training to fall back on. Frontier models like Claude Opus or GPT-4o can compensate for lower-quality RAG, but they&rsquo;re expensive. Choose wisely based on your use case and budget.</p> <h2 id="the-road-forward" class="relative group">The road forward <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-road-forward" aria-label="Anchor">#</a></span></h2><p>After navigating the challenges, here are the critical lessons for building production RAG:</p> <p><strong>Start with a clear user story.</strong> Define who will use your system, what they&rsquo;ll do with it, and what benefit they&rsquo;ll get. This becomes your north star when making tough decisions.</p> <p><strong>Build a continuous improvement pipeline.</strong> Set up a process to identify knowledge gaps, refine documents, score results, and only promote good content to production. RAG isn&rsquo;t a deploy-and-forget solution.</p> <p><strong>Measure everything that matters.</strong> Log queries, responses, similarity scores, and user feedback. You can&rsquo;t improve what you don&rsquo;t measure.</p> <p><strong>Documentation quality trumps RAG sophistication.</strong> The best RAG system in the world can&rsquo;t fix terrible documentation. Garbage in, garbage out โ€“ always.</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="960" height="540" class="mx-auto my-0 rounded-md" alt="takeaways.png" loading="lazy" decoding="async" src="https://major.io/p/devconf-rag/takeaways_hu_e1764c90eae580cc.png" srcset="https://major.io/p/devconf-rag/takeaways_hu_166e0ff969a5fed0.png 330w,https://major.io/p/devconf-rag/takeaways_hu_e1764c90eae580cc.png 660w ,https://major.io/p/devconf-rag/takeaways.png 960w ,https://major.io/p/devconf-rag/takeaways.png 960w " sizes="100vw" /> </picture> </figure> </p> <h2 id="key-takeaway" class="relative group">Key takeaway <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#key-takeaway" aria-label="Anchor">#</a></span></h2><p>If there&rsquo;s one thing to remember from my talk, it&rsquo;s this: RAG is not a destination but an ongoing quest. The perfect solution is the enemy of progress. Start small, fail fast, learn constantly, and iterate relentlessly.</p> <p>The slides from my presentation are available in the PDF file in this repository, and I encourage you to check them out for the full Lord of the Rings journey through RAG implementation.</p> <p>Building production RAG systems taught me that success comes not from following the latest HackerNews trends, but from understanding your users, respecting the complexity, and assembling the right fellowship for the journey.</p> <p>Stay on the path, and may your RAG responses be ever accurate.</p>Automatic container updates with Podman quadletshttps://major.io/p/podman-quadlet-automatic-updates/Fri, 19 Sep 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/podman-quadlet-automatic-updates/<p>Running containers at home or in production often means juggling updates across multiple services. While orchestration platforms like Kubernetes handle this automatically, what about those simple deployments on a single host?</p> <p>Podman&rsquo;s quadlet system integrates containers directly with systemd, and when combined with automatic updates, you get a robust solution that keeps your containers fresh without manual intervention.</p> <p>Let&rsquo;s explore how to set up automatic container updates using Podman quadlets on Fedora, turning container management into a hands-off operation that just works.</p> <h2 id="setting-up-a-basic-quadlet" class="relative group">Setting up a basic quadlet <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#setting-up-a-basic-quadlet" aria-label="Anchor">#</a></span></h2><p>First, let&rsquo;s create a simple quadlet for running a Valkey database service under a user account. Quadlet files for user services live in <code>~/.config/containers/systemd/</code>.</p> <p>Create the directory if it doesn&rsquo;t exist:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.config/containers/systemd/ </span></span></code></pre></div><p>Then create your quadlet file:</p> <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/containers/systemd/valkey.container</span> </span></span><span class="line"><span class="cl"><span class="k">[Container]</span> </span></span><span class="line"><span class="cl"><span class="na">ContainerName</span><span class="o">=</span><span class="s">valkey</span> </span></span><span class="line"><span class="cl"><span class="na">Image</span><span class="o">=</span><span class="s">docker.io/valkey/valkey:latest</span> </span></span><span class="line"><span class="cl"><span class="na">Label</span><span class="o">=</span><span class="s">io.containers.autoupdate=registry</span> </span></span><span class="line"><span class="cl"><span class="na">PublishPort</span><span class="o">=</span><span class="s">16379:6379</span> </span></span><span class="line"><span class="cl"><span class="na">Volume</span><span class="o">=</span><span class="s">valkey_data:/data</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">Restart</span><span class="o">=</span><span class="s">always</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">default.target</span> </span></span></code></pre></div><p>The magic happens with the <code>Label=io.containers.autoupdate=registry</code> line. This label tells Podman that this container should be automatically updated when a newer image is available in the registry.</p> <p>After creating the file, reload systemd and start your container:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user daemon-reload </span></span><span class="line"><span class="cl">systemctl --user start valkey.service </span></span></code></pre></div><p>Your container is now running as a user systemd service! Check its status with:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user status valkey.service </span></span><span class="line"><span class="cl">podman ps </span></span></code></pre></div><h2 id="enabling-automatic-updates" class="relative group">Enabling automatic updates <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#enabling-automatic-updates" aria-label="Anchor">#</a></span></h2><p>Podman ships with a systemd timer that checks for container updates. The <code>podman-auto-update.timer</code> runs daily by default, but you need to enable it for your user:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now podman-auto-update.timer </span></span></code></pre></div><p>You can check when the next update will run:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user list-timers podman-auto-update.timer </span></span></code></pre></div><p>When the timer triggers, it runs <code>podman auto-update</code>, which:</p> <ol> <li>Checks all containers with the <code>io.containers.autoupdate</code> label</li> <li>Pulls newer images if available</li> <li>Restarts containers with the new image</li> <li>Keeps the old image in case you need to roll back</li> </ol> <h2 id="customizing-update-behavior" class="relative group">Customizing update behavior <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#customizing-update-behavior" aria-label="Anchor">#</a></span></h2><p>The <code>io.containers.autoupdate</code> label supports different values for various update strategies:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># Always pull the latest image from the registry</span> </span></span><span class="line"><span class="cl"><span class="na">Label</span><span class="o">=</span><span class="s">io.containers.autoupdate=registry</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Only update if the local image changes (useful for locally built images)</span> </span></span><span class="line"><span class="cl"><span class="na">Label</span><span class="o">=</span><span class="s">io.containers.autoupdate=local</span> </span></span></code></pre></div><p>You can also customize when updates occur by creating a timer override:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user edit podman-auto-update.timer </span></span></code></pre></div><p>Add these lines to run updates every 6 hours instead of daily:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Timer]</span> </span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span> </span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">*-*-* 00,06,12,18:00:00</span> </span></span></code></pre></div><h2 id="monitoring-updates" class="relative group">Monitoring updates <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#monitoring-updates" aria-label="Anchor">#</a></span></h2><p>Track what&rsquo;s happening with your automatic updates using journalctl:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># View recent auto-update logs</span> </span></span><span class="line"><span class="cl">journalctl --user -u podman-auto-update.service -n <span class="m">50</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Follow updates in real-time</span> </span></span><span class="line"><span class="cl">journalctl --user -u podman-auto-update.service -f </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Check a specific container&#39;s restart history</span> </span></span><span class="line"><span class="cl">journalctl --user -u valkey.service <span class="p">|</span> grep Started </span></span></code></pre></div><h2 id="further-reading" class="relative group">Further reading <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#further-reading" aria-label="Anchor">#</a></span></h2><ul> <li><a href="https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html" target="_blank" rel="noreferrer">Podman documentation on auto-updates</a> - Official documentation for the auto-update feature</li> <li><a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html" target="_blank" rel="noreferrer">Systemd Quadlet documentation</a> - Complete reference for quadlet unit files</li> <li><a href="https://www.redhat.com/sysadmin/quadlet-podman" target="_blank" rel="noreferrer">Red Hat&rsquo;s guide to Podman quadlets</a> - Excellent introduction with more examples</li> </ul>Date driven developmenthttps://major.io/p/date-driven-development/Sun, 24 Aug 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/date-driven-development/<p>Scrum, kanban, and waterfall seem familiar to most of us who work in software development, but date driven development is a complicated beast. These are situations where something <em>must be completed</em> by a certain date that cannot be moved.</p> <p>For some teams, this could be a product launch at a big company event. An executive might need to present something to investors or analysts. The team might need to have something ready for multiple other teams to use in their own work.</p> <p>Although these situations can be stressful, it&rsquo;s one of those great opportunities where everyone has a chance to shine! This post covers some techniques and strategies that you can use (at any level) to help your team reach the finish line.</p> <h1 id="what-goes-on-the-truck" class="relative group">What goes on the truck? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-goes-on-the-truck" aria-label="Anchor">#</a></span></h1><p>A talented executive once took me under their wing and explained how to inspire a team to deliver results with a difficult deadline. He would always ask, <strong>&ldquo;What goes on the truck?&rdquo;</strong></p> <p>Imagine a truck delivering your product to the big event. There are a lots of different outcomes, but I&rsquo;ll boil them down to these three:</p> <ol> <li><strong>The truck arrives on time with everything needed to be successful.</strong> We all want this outcome, but it&rsquo;s not always possible. This should be your team&rsquo;s main goal and focus.</li> <li><strong>The truck arrives on time with all of the required items, but some extras are missing.</strong> Most of the teams I&rsquo;ve worked with have landed here. Everything that the customer needs is on the truck, but some of the nice-to-haves are missing. Some of these items could be a &ldquo;fast follow&rdquo; that arrive after the event.</li> <li><strong>The truck doesn&rsquo;t arrive.</strong> You don&rsquo;t want this.</li> </ol> <p>I once went to a fast food restaurant to order a burger and fries. After sitting down with my drink, I waited for my food and it felt like I waited for a long time. Someone came over and said:</p> <blockquote> <p>&ldquo;Here&rsquo;s your burger, but the fries are running behind. We have a new fry cook and she&rsquo;s still learning. I&rsquo;ll have your fries over as soon as they&rsquo;re done.&rdquo;</p></blockquote> <p>What happened there? I received the most important item (the burger), but not the extras (the fries). That was fine since I came to the restaurant for a burger anyway and the fries were just a nice-to-have. The fries were on the way, but it didn&rsquo;t interrupt the enjoyment of eating the burger. The fries came along shortly and I was happy.</p> <p>You can do the same thing with your product:</p> <ol> <li>Deliver the base functionality that provides value for your customer.</li> <li>Ship as many extras as you can. Identify the extras that deliver the most value for the least amount of effort. Work on those first.</li> <li>Be willing to drop an extra or say &ldquo;no&rdquo; to something that could jeopardize the delivery of the base product.</li> </ol> <p>All of this works a lot better if you communicate often about the extras, the value they provide, and the development effort required. That&rsquo;s the next section!</p> <h1 id="clustered-candid-communication" class="relative group">Clustered, candid communication <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#clustered-candid-communication" aria-label="Anchor">#</a></span></h1><p>Everyone on the team and everyone who the team depends on must communicate effectively &ndash; <strong>and often.</strong> One of the biggest downfalls for any team under stress is a lack of communication. This causes a mismatch in expectations within the group.</p> <p>Here are a few simple things you can do to improve your team&rsquo;s communication:</p> <ol> <li><strong>Talk about all deadlines often.</strong> This keeps everyone on the team in the loop about important dates, especially those they might not be aware of. For example, development teams might not know when quality engineering teams need final builds to test. Product managers need early builds to share with pilot customers and sales teams.</li> <li><strong>Cluster your communication.</strong> Interruptions throw a wrench into a team&rsquo;s productivity. Cluster communications to a certain time of day or a certain day of the week. Every meeting should have a detailed agenda and clear outcomes. Each attendee must know what is expected of them at each meeting.</li> <li><strong>Prioritize blockers.</strong> If someone on the team is blocked, they need to communicate that immediately. Treat these problems with the highest priority and identify what you need to resolve the issue. Blockers are extremely demotivating and can cause issues to fall through the cracks.</li> <li><strong>If you see something, say something.</strong> Encourage everyone to ask questions and raise their concerns. I&rsquo;ve had good experiences with an &ldquo;around-the-horn&rdquo; approach at the end of a meeting where the organizer calls out attendees one by one to to see how they&rsquo;re feeling.</li> </ol> <p>Don&rsquo;t forget about communication outside the team!</p> <p>For outbound communication, find ways to share meaningful status updates with other groups and leaders. This can head off unnecessary interruptions by allowing people to quickly scan a status update.</p> <p>Assign an point person to handle inbound communications from other teams and rotate that role often (weekly or bi-weekly). This person can triage issues and raise concerns with the team when necessary without distrupting the team&rsquo;s focus.</p> <h1 id="set-the-ground-rules" class="relative group">Set the ground rules <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#set-the-ground-rules" aria-label="Anchor">#</a></span></h1><p>This one is often forgotten. It&rsquo;s important to specify what is negotiable and what isn&rsquo;t as you start the project.</p> <p>For example, during the early days of the project, teams might be allowed to merge code without full reviews or complete test coverage. As times goes on, the team might tighten the rules, requiring more strict reviews and increased test coverage. Teams might also forego performance optimizations during the early stages so that the base functionality gets done quickly.</p> <p>Some good ground rule strategies include:</p> <ol> <li><strong>Get agreement before starting.</strong> Set some milestones and the requirements at each step. Be sure that each team member knows what is expected of them whether they&rsquo;re proposing a change or reviewing that change before a merge.</li> <li><strong>Set build versus buy requirements.</strong> It&rsquo;s often much easier to add modules or libraries written by other people to speed up the development of a project. Some of these are simple, such as HTTP libraries or logging frameworks. However, if a team is going to adopt something significant, such as a database or a UI framework, set some ground rules about evaluating those options.</li> <li><strong>Identify who is responsible for what.</strong> A <a href="https://en.wikipedia.org/wiki/Responsibility_assignment_matrix" target="_blank" rel="noreferrer">RACI matrix</a> is extremely helpful here. These simple charts identify everyone&rsquo;s role in a project, or for a piece of a project. Setting this up early avoids turf battles or situations where someone feels like they might be stepping on someone else&rsquo;s toes. It also identifies the right people for a potential escalation or decision.</li> </ol> <p>Speaking of escalation, let&rsquo;s get to the last section!</p> <h1 id="escalate-early-and-effectively" class="relative group">Escalate early and effectively <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#escalate-early-and-effectively" aria-label="Anchor">#</a></span></h1><p>Most date driven development projects have a lot of visibility, especially with leaders. It&rsquo;s just as critical for leaders to manage the people on the project as it for those people to manage their own leaders.</p> <p>The last thing any VP wants to hear is that a project is in trouble because the team needed something trivial to complete it. These upward communications are tricky. Every company has their own protocols and culture, and this sometimes is specific to certain parts of a company. However, in my experience, there are some universal things that work well:</p> <ol> <li><strong>Escalate early.</strong> Do not wait. Seriously. If there&rsquo;s a blocker that might prevent the team from delivering, let your leaders know about it. It&rsquo;s much better to escalate several silly things early than it is to escalate one critical thing late.</li> <li><strong>Give a little context.</strong> Explain exactly what is needed and any costs involved. Be sure to include what will happen if the issue is or is not resolved. Although this might require a little explanation of the background, keep it brief and focus on the aspects your leaders care the most about.</li> <li><strong>Prove your assessment.</strong> A good friend of mine said &ldquo;lead with the outcome you want.&rdquo; Once an executive knows the context, they&rsquo;re looking for your expert guidance on what they should do. Empower them to make the right decision by sharing your assessment of the best course of action along with some alternatives and the disadvantages of each.</li> <li><strong>Set a deadline.</strong> Your leaders have tons of decisions to make with varying timelines and priorities. There&rsquo;s a big difference between &ldquo;we need this by the end of the day&rdquo; and &ldquo;we need this by the end of the month.&rdquo;</li> </ol> <p>The <a href="https://major.io/p/raise-the-bar-with-an-sbar/">SBAR</a> document gives you a great way to structure your communication in concise way that leaders can quickly understand. It gives them an understanding of what you&rsquo;re doing, the problem you&rsquo;re up against, and your assessment of the available options. Just remember to keep it to one page and focus on the things your leaders care about the most.</p> <h1 id="stuff-happens" class="relative group">Stuff happens <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#stuff-happens" aria-label="Anchor">#</a></span></h1><p>No matter what you do, stuff happens. Systems break. People get sick. Plans change.</p> <p>Do your best to be flexible and set the right expectations. One of my childrens&rsquo; preschools had a great motto around snack time that applies here:</p> <blockquote> <p>You get what you get and you don&rsquo;t throw a fit.</p></blockquote> <p>Your team can only deliver so much. Take any shortcomings or feedback as a learning opportunity for next time. Set up retrospective meetings to identify what went well and what didn&rsquo;t. Be sure to share these learnings with other teams, too!</p> <p>Finally, <strong>take time to celebrate the win when you finish.</strong> I recently worked with a great team on a product that launched during a big keynote presentation at a company event and it was an amazing feeling. Sure, our product had plenty of rough edges and room for improvement, but at that moment, we delivered.</p>Scrum, sprints, and outcomeshttps://major.io/p/scrum-sprints-outcomes/Sun, 01 Jun 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/scrum-sprints-outcomes/<p>Most software developers have come across <a href="https://en.wikipedia.org/wiki/Agile_software_development" target="_blank" rel="noreferrer">agile software methodologies</a> such as <a href="https://en.wikipedia.org/wiki/Scrum_%28software_development%29" target="_blank" rel="noreferrer">Scrum</a>. At its core, Scrum&rsquo;s goal is to help teams deliver software in smaller chunks over a set period of time, called a sprint. Teams should be able to work better together, deliver more frequently, and adapt to changes more easily.</p> <p><strong>However, Scrum often becomes a theater of activity &ndash; story points, velocity charts, and ceremony compliance &ndash; that distracts teams from what customers actually need.</strong></p> <h1 id="what-is-scrum" class="relative group">What is Scrum? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-is-scrum" aria-label="Anchor">#</a></span></h1><p>This could be a whole post in itself, but in my experience, Scrum revolves around a few core tenets:</p> <ul> <li><strong>The work.</strong> Development, quality, and research work is time-boxed into a sprint, most often lasting two weeks. You cobble together a list of tasks (or tickets) together that you believe you can complete within the sprint.</li> <li><strong>The team.</strong> There&rsquo;s obviously a development team, but there are two other roles involved. The product owner helps with prioritizing work, organizing the backlog of to-do items, and ensuring that the team is working on the right things to deliver value for customers. A scrum master is almost like a specialized project manager who helps the team stay on track, removes roadblocks, and ensures that the team is following Scrum practices.</li> <li><strong>The ceremonies.</strong> I use <em>ceremony</em> here slightly facetiously, but most Scrum teams have a set of meetings that they hold regularly. There are daily standups, sprint planning meetings, sprint reviews, and retrospectives. The goal is to make the next sprint better than the previous one and surface problems.</li> </ul> <p>When this works well, most people on the team are aware of what others are working on and where they might be struggling. Other adjacent teams, such as marketing or sales, can align their work so that they&rsquo;re fully prepared to bring those products or improvements into customers&rsquo; hands.</p> <p><strong>It&rsquo;s not all a panacea.</strong> Let&rsquo;s discuss why.</p> <h1 id="estimations-and-exploratory-work" class="relative group">Estimations and exploratory work <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#estimations-and-exploratory-work" aria-label="Anchor">#</a></span></h1><p>The world has many, many trades where you can estimate the time and complexity of your work before getting started. If you call a plumber for a leaky pipe, they can estimate the cost and time required to fix it with reasonable accuracy. When your car has a flat tire, the mechanic can tell you how long it will take to fix it and how much it will cost.</p> <p><strong>Software development is inherently <em>exploratory</em>.</strong> That&rsquo;s because it involves building something that didn&rsquo;t exist previously. Sometimes there are tools and libraries that speed up development, but these often have limitations that require research.</p> <p>As an example, I recently worked on a project that involved adding lots of documents to a retrieval-augmented generation (RAG) system. This is a method for giving an AI model access to information to answer questions that it didn&rsquo;t know already.</p> <p>Another team had great results with a particular database and mechanism for adding documents, so I was able to utilize most of their strategy. Everything looked like it would be straightforward. An easy ticket with a low estimate; who doesn&rsquo;t love that?</p> <p>Then things changed.</p> <ul> <li>The database worked well for their use case, but I had no idea that they were working with a small number of high quality documents in a consistent format. My documents had varying structure, varying quality, and we had a lot more of them.</li> <li>Our RAG database build times were much longer than theirs, so we had to search for infrastructure that would allow us to build ours faster.</li> <li>Their embedding model for turning text into vectors for a semantic search worked well for their documents but it didn&rsquo;t work for ours.</li> <li>We found a better database, but then we had to find out how we could deploy and manage it following our company&rsquo;s procedures.</li> </ul> <p>What we thought would be done in a couple of weeks suddenly took a couple of months. We had to come up with new strategies and also think differently about what we would do before and after the RAG search to improve its quality.</p> <p><strong>Time-boxing this work into a sprint was extremely difficult.</strong> Estimation was even more difficult because we didn&rsquo;t know the complexity involved and how long it would take. Some of the complexity questions couldn&rsquo;t even be answered because we had dependencies on other teams to complete the work.</p> <p>This means we were breaking Scrum&rsquo;s core tenets constantly to deliver the features:</p> <ul> <li>We added tickets to sprints after the sprint opened</li> <li>We carried lots of issues over to the next sprint as we added new tickets to that sprint</li> <li>Estimates were wildly inaccurate</li> </ul> <p>In the end, we did deliver what was needed, but we often wondered why we were still putting such a heavy emphasis on Scrum.</p> <h1 id="interruptions" class="relative group">Interruptions <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#interruptions" aria-label="Anchor">#</a></span></h1><p>Interruptions are a fact of life. The Linux kernel deals with this via aptly named interrupts, and it receives signals that something needs attention. It could be events coming from a keyboard or mouse, network packets waiting to be processed, or any number of timers running on the system. The kernel is well suited to handle many of these smaller interrupts. Larger ones lead to heavy context switching and these strain the system.</p> <p>Software developers are no different. I work with brand new software developers and interns who need guidance from time to time on how to attack a problem. These interruptions are great! Someone gains a new skill and can do more than they previously could. I also reinforce my own knowledge by teaching them. It&rsquo;s usually on a topic that I know well and often tangentially related to what I&rsquo;m working on anyway.</p> <p>Getting into a meeting with multiple engineers on Google Meet to argue about story points, burn down charts, sprint velocity, and other metrics is a different story entirely.</p> <p>During a two week sprint at various companies, I&rsquo;ve found myself spending a decent amount of time doing things other than software development:</p> <ul> <li><strong>Daily standups.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></strong> Everyone shares what they completed yesterday, what they completed today, and any blockers they have.</li> <li><strong>Sprint planning meetings.</strong> The team brings together their tickets to plan out what will be included in the next sprint. This is often when a sprint is closed and the next one is opened. You have these every two weeks if you&rsquo;re doing two week sprints.</li> <li><strong>Sprint reviews and demonstrations (demos).</strong> Some organizations combine these two together, but the goal is to share what was completed during the sprint and demonstrate how any new features work.</li> <li><strong>Retrospective.</strong> Once a sprint has finished, the team meets to discuss what worked, what didn&rsquo;t work, and what needs to be changed in the next sprint. These often occur once the next sprint has already started, so the team is already in the middle of the next sprint while discussing the previous one.</li> </ul> <p>Standups are excellent, especially for teams with new developers. The other meetings can quickly become a burden.</p> <p>If we assume standups are 10 minutes per day and the remaining meetings all last an hour each, that&rsquo;s just under five hours of meetings per two week sprint. That doesn&rsquo;t include other meetings, such as team meetings, company wide meetings, one-on-ones, and mentorship.</p> <p>You might think that&rsquo;s not terrible, but consider that these tickets must be written, estimated, reviewed, and prioritized prior to the sprint planning meeting. Demos must be built and prepared for the sprint review. Retrospectives require the team to think ahead of time about their work before the meeting and then think about how to implement changes after the meeting.</p> <p>These interruptions can quickly stack up. <strong>When you combine software development&rsquo;s exploratory nature with constant ceremony interruptions, you create a recipe for burnout.</strong> We should be focused on delivering an outcome or helping a teammate deliver an outcome. Scheduling these around sprint ceremonies wastes time and energy.</p> <h1 id="activity-over-outcomes" class="relative group">Activity over outcomes <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#activity-over-outcomes" aria-label="Anchor">#</a></span></h1><p>When you sit down in your new car for the first time or hold that latest smartphone, you&rsquo;re probably not wondering how many sprints it took to build it. The look of the company&rsquo;s velocity or burn down charts probably won&rsquo;t cross your mind.</p> <p><strong>As the customer, you&rsquo;re focused on the outcome.</strong></p> <p>You name it, I&rsquo;ve probably done it at one company or another. Scrum. Waterfall. Continuous flow. Kanban.</p> <p>Any of these turns toxic as soon as the focus is on the activity rather than the outcome. Focusing on the activity means putting tons of weight on the processes, the meetings, and the metrics. It means you <em>say you&rsquo;re interested in the outcomes</em>, but you don&rsquo;t practice that from day to day.</p> <p>If you want to ensure teams deliver process compliance instead of customer value, lean in really hard on the agile process. Developers work around these processes by doing quite a few unhelpful things:</p> <ul> <li>Avoid creating tickets or create far too many tickets.</li> <li>Locking themselves into a solution prematurely to ensure something gets done within the sprint.</li> <li>Sandbagging estimates to buy time or make things fit into a sprint.</li> <li>They redefine what &ldquo;done&rdquo; means and then add bugs or refinements to later sprints.</li> <li>Turn Scrum processes into &ldquo;meeting theater&rdquo; where everyone goes through the motions but nobody really cares about the outcome.</li> </ul> <p>None of these benefit the team, the leaders, or the end customer. It also pushes developers to look for other teams or other companies that are more focused on outcomes.</p> <p><strong>The honest answer is that nobody knows when software will ship.</strong> No matter what methodology you use or how hard you push on the agile process, you can&rsquo;t predict the future<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. Software development is complex, customer demands change constantly, and new technologies emerge daily. What makes sense on day one of the sprint may not make sense on day 14.</p> <p>It doesn&rsquo;t have to be this way.</p> <h1 id="what-do-we-do" class="relative group">What do we do? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-do-we-do" aria-label="Anchor">#</a></span></h1><p><strong>Everything that a team does must be focused on outcomes.</strong> This isn&rsquo;t the activity that delivers an outcome, but the outcome itself.</p> <p>In the past, I worked on a team where we ran Kanban instead of Scrum and we had a &ldquo;theme&rdquo; that we worked toward. Kanban is more of a continuous flow methodology with a limit on work in progress items and without a defined time for work. It wasn&rsquo;t the methodology that made us successful. It was the focus on the theme.</p> <p>For example, we had some themes such as &ldquo;deliver feature X&rdquo; or &ldquo;improve cost efficiency of Y to x%&rdquo;. Everyone on the team, including developers, quality engineers, and documentation experts all knew the goal. We could work on whatever we needed to in order to achieve that goal but we could not exceed our work in progress (WIP) limits.</p> <p>As you might expect, someone said <em>&ldquo;Hey, what happens if we hit the WIP limit?&rdquo;</em> Our astute manager at the time knew she had hired talented people who are great at solving tough problems and she was ready with her answer: <em>&ldquo;That&rsquo;s for you to figure out.&rdquo;</em></p> <p>Something interesting happened when we hit the WIP limit for the first time. Sure, the column in Jira turned red and someone mentioned it in Slack, but that isn&rsquo;t what I&rsquo;m talking about. Someone was freed up on their task and realized they couldn&rsquo;t pull anything else into the &ldquo;in progress&rdquo; column.</p> <p>They looked in the column for a minute and discovered an issue that was really familiar to them, but it was assigned to someone else. They asked the person working on it if they needed help and the person assigned to the ticket said: <em>&ldquo;Yeah, I think I&rsquo;m stuck!&rdquo;</em> It turned out to be a great teaching and mentorship opportunity.</p> <p>We saw several benefits from being focused on the outcome and not the activity:</p> <ul> <li>The &ldquo;stuck&rdquo; issue didn&rsquo;t appear in the daily standup because the developer was afraid to raise it. That fear was solved by another developer joining in when the WIP limit was hit.</li> <li>Team members focused heavily on the &ldquo;in progress&rdquo; column and we discovered that we didn&rsquo;t need standups as often. The goal changed to &ldquo;let&rsquo;s figure out these stuck issues&rdquo; organically.</li> <li>The theme was our &ldquo;rally cry&rdquo; going forward. We all knew what we were working towards and why we were doing it.</li> <li>Estimating issues turned into more of a discussion of what was involved in solving the issue instead of an exercise in futility.</li> <li>Our product owner only needed to ensure the &ldquo;to do&rdquo; column was cleaned up and prioritized. Everyone knew where they needed to pull work from first.</li> </ul> <p>We still had date constraints, but the <strong>dates were related to when we needed to deliver something to the customer</strong>, not arbitrary dates that we set for ourselves. We all knew the dates, the goal, and the overall mission. We knew the what, the how, and the why.</p> <h1 id="summary" class="relative group">Summary <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor">#</a></span></h1><p>Scrum often transforms software teams into process performers rather than problem solvers. Lighter processes that reinforce the <em>right behaviors</em> deliver more value for teams and helps them focus on outcomes. When teams are focused on outcomes, they can adapt to changes, solve problems, and deliver more value to customers. When teams know their &ldquo;why,&rdquo; they&rsquo;ll figure out the &ldquo;what&rdquo; and &ldquo;how&rdquo; without rigid processes.</p> <div class="footnotes" role="doc-endnotes"> <hr> <ol> <li id="fn:1"> <p>There are synchronous (everyone gets in a meeting together at the same time) and asynchronous (everyone puts their updates in a central place for later review). The async standups definitely save time, but then you lose the ability to ask questions and many developers forget to read the updates.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> <li id="fn:2"> <p>There is a concept of &ldquo;date driven development&rdquo; where something must ship on time, and in that case, you can drop features or capabilities to ensure on time delivery. You just don&rsquo;t be sure how many features and capabilities the product will have when it ships.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> </ol> </div>Vibe-free coding with AIhttps://major.io/p/vibe-free-coding-with-ai/Wed, 07 May 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/vibe-free-coding-with-ai/<p>The internet has been in quite a ruckus about <em>vibe coding</em> recently. Heck, there&rsquo;s already a <a href="https://en.wikipedia.org/wiki/Vibe_coding" target="_blank" rel="noreferrer">Wikipedia</a> page about it! It must be real if it has a Wikipedia page! ๐Ÿ˜†</p> <p>Long story short, vibe coding involves asking a large language model (LLM), the foundation of an AI platform, to write code for you. LLMs are actually quite good at writing code. It turns out that they&rsquo;re often terrible at understanding nuance or stitching complex code relationships together.</p> <p>I use AI all the time when I write software, but I wouldn&rsquo;t call it <em>vibe coding</em>. LLMs in my development environment help me catch errors more quickly, highlight improvements, and reduce time spent on very tedious tasks.</p> <p>This post covers my use cases for AI while writing code and my current setup.</p> <h1 id="my-setup" class="relative group">My setup <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#my-setup" aria-label="Anchor">#</a></span></h1><p>About 90% of my development happens in Visual Studio Code, or VS Code. The rest is in vim. There&rsquo;s a helpful <a href="https://code.visualstudio.com/docs/copilot/overview" target="_blank" rel="noreferrer">GitHub Copilot extension for VS Code</a> that integrates really well to help with errors, give auto-complete suggestions, and answer questions.</p> <p>GitHub offers a totally free GitHub Copilot subscription, but if you&rsquo;re a maintainer of a popular open source project or an avid open source contributor, they currently offer <a href="https://docs.github.com/en/copilot/managing-copilot/managing-copilot-as-an-individual-subscriber/getting-started-with-copilot-on-your-personal-account/about-individual-copilot-plans-and-benefits" target="_blank" rel="noreferrer">Pro subscriptions</a> at no cost. This free Pro offering could change at any time, so be prepared for that. ๐Ÿ˜‰</p> <h1 id="ai-use-cases" class="relative group">AI use cases <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ai-use-cases" aria-label="Anchor">#</a></span></h1><p>I use AI in several different ways when I write code.</p> <h2 id="fixing-errors" class="relative group">Fixing errors <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#fixing-errors" aria-label="Anchor">#</a></span></h2><p>Most of my development is done in Python and I&rsquo;m almost always coding with <code>ruff</code> and <code>mypy</code>. This means that I run into some linting problems from time to time and sometimes I forget to put a type annotation for functions or arguments. Existing non-AI plugins catch most of these mistakes and usually the fixes are easy. Sometimes they&rsquo;re difficult.</p> <p>For those difficult times, it&rsquo;s handy to ask for some AI help, especially when I haven&rsquo;t used a specific python module before. VS Code gives me a little sparkle emoji underneath the function name and offers to fix the problem for me.</p> <p>This has helped recently at work as I&rsquo;ve worked with llama-index extensively and determining which type is returned from a function can be challenging. Sometimes it&rsquo;s a base model being returned, but sometimes it&rsquo;s a different class that inherits a base class. Sure, I could dig through documentation or wade through the llama-index code for that, but that&rsquo;s tedious work. I&rsquo;d rather get a suggestion from Copilot and confirm that it&rsquo;s correct. That&rsquo;s a lot easier than digging through the code to find the right answer.</p> <h2 id="understanding" class="relative group">Understanding <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#understanding" aria-label="Anchor">#</a></span></h2><p>How many times have you been working on a complex project and you think:</p> <blockquote> <p>Who wrote this? I have no idea what&rsquo;s going on. Why is this even here?</p></blockquote> <p>I&rsquo;ll often highlight the code in question and ask for an interpretation from the LLM. Since Copilot has access to more files in the project, it&rsquo;s able to connect the dots with functions and methods from other files.</p> <p>This helps me better understand the project in less time. It also helps me learn new methods for doing things (even if those methods might be terrible).</p> <h2 id="testing" class="relative group">Testing <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#testing" aria-label="Anchor">#</a></span></h2><p>Errors sometimes still sneak into the code even with careful linting and strict type checking. I&rsquo;ll often ask Copilot to write a test for a function for me with branch coverage. Branch coverage catches those situations where code might be skipped based on an <code>if</code>/<code>else</code> clause and it ensures you&rsquo;re testing all possible code paths. Sometimes Copilot will write a test that checks a condition I didn&rsquo;t consider. That&rsquo;s a great opportunity to return to the original code and think through the logic once more.</p> <p>There are other situations where a test fails and it&rsquo;s difficult to understand why. I&rsquo;ll usually bring up the test on the right and the code on the left to think through the code path in my head. Then there are those times where I ask Copilot to give a suggestion and it points to the exact spot in my code where I didn&rsquo;t consider a specific condition. Strings and byte strings catch me offsides quite often. ๐Ÿคญ</p> <h2 id="improvements" class="relative group">Improvements <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#improvements" aria-label="Anchor">#</a></span></h2><p>We&rsquo;ve all been in that situation where we know what needs to be done, we write a few functions, and then think &ldquo;Gosh, that seems like too much code&rdquo; or &ldquo;That looks convoluted&rdquo;. I&rsquo;ll sometimes ask Copilot for a suggestion to simplify a function or block of code. I rarely take the whole suggestion, but it reminds me of patterns I&rsquo;ve forgotten or it introduces me to new ones I haven&rsquo;t seen before.</p> <p>As an example, I was working on some async python code that needed to be wrapped in a timeout. The timeout seemed to be working fine, but then the original function being awaited kept running until it timed out later. That caused exceptions to be thrown via Sentry and it was extremely annoying.</p> <p>What I really needed is a way to stop the awaited function once the timeout wrapper was reached.</p> <p>I asked Copilot for help with something like:</p> <blockquote> <p>This function keeps running after the timeout is reached and it causes another exception. I need to kill the awaited function as soon as the timeout is reached.</p></blockquote> <p>Sure enough, Copilot came back with a suggestion to use <a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.wait_for" target="_blank" rel="noreferrer"><code>asyncio.wait_for</code></a>. I&rsquo;d never seen that before! The docs highlighted the big difference between <code>wait</code> and <code>wait_for</code>:</p> <blockquote> <p>If a timeout occurs, it cancels the task and raises TimeoutError.</p></blockquote> <p>Perfect! ๐ŸŽ‰</p> <h2 id="tedious-tasks" class="relative group">Tedious tasks <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#tedious-tasks" aria-label="Anchor">#</a></span></h2><p>There are many occasions where Copilot guesses what I&rsquo;m thinking next, especially as I build out scaffolding for a new class or write documentation for a function. As an example, I was writing an OpenShift template last month and I was bringing over some templated <code>Deployment</code> and <code>Service</code> definitions. I customized the template with lots of variables for the image source, environment variables, and volume mounts.</p> <p>OpenShift templates have a long section at the bottom where you define the default values for the variables used in your template. As I began typing the first variable name, Copilot suggested the variable name, a short definition, and the default value. The default value was correct, but the description was a little off. After a quick fix of the description, I moved to the next variable and Copilot started filling in the next one from the template. I gradually just tab completed the remainder of the file.</p> <p>Many of the descriptions needed some tweaks here and there and the default variables needed to be updated. However, all of that structure that I&rsquo;d be copying and pasting repeatedly was all done for me. That saved me plenty of time and reduced the chance of making a syntax error.</p> <h1 id="conclusion" class="relative group">Conclusion <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#conclusion" aria-label="Anchor">#</a></span></h1><p>GitHub Copilot in VS Code feels like a partner that is looking over my shoulder as I work. I can choose when to engage with a suggestion or ask for additional help. It&rsquo;s also a great way to get un-stuck when you&rsquo;re in a tight spot.</p> <p><strong>None of this is a replacement for fully understanding what you&rsquo;re being asked to write and being knowledgeable about how your application fits together.</strong></p> <p>Some might argue that an AI coding assistant is some kind of <em>crutch</em><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. I&rsquo;d argue that this depends completely on how you use it. If you use it to write code for you without understanding the code, then yes, it&rsquo;s a crutch.</p> <p>If you use it to help you understand the code, catch errors, and improve your code, then it&rsquo;s a useful virtual partner in your coding adventures. ๐Ÿง—โ€โ™‚๏ธ</p> <div class="footnotes" role="doc-endnotes"> <hr> <ol> <li id="fn:1"> <p>In the realm of US English, a crutch is a device that helps you walk when you have an injury. Many people use it as a metaphor for something that helps you do something you can&rsquo;t do on your own. I&rsquo;ve had people tell me that using VS Code is a crutch. Some people even say that <strong>vim</strong> is a crutch.</p> <p><em>&ldquo;You should juse use <code>ed</code> and enjoy it!&rdquo;</em> ๐Ÿ˜†&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> </ol> </div>Don't tell me RAG is easyhttps://major.io/p/dont-tell-me-rag-is-easy/Fri, 18 Apr 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/dont-tell-me-rag-is-easy/<p>Blog posts have been moving slowly here lately and much of that is due to work demands since the end of 2024. I&rsquo;ve been working on an AI-related product with a talented team of people and we learned plenty of lessons about retrieval-augmented generation, or RAG.</p> <p>This post covers the basics of RAG, some assumptions I made, and what I&rsquo;ve learned.</p> <h2 id="what-is-rag" class="relative group">What is RAG? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-is-rag" aria-label="Anchor">#</a></span></h2><p>Large language models, or LLMs, are trained on huge amounts of information. This information could come from just about anywhere, including books, online resources, and even this blog! There are plenty of ethical questions here, especially around LLM providers that train their models on copyrighted or otherwise restricted material. They gain ground on their competitors in the short run, but this is not ideal in the long run.</p> <p>Sometimes training a model isn&rsquo;t feasible. That&rsquo;s where RAG comes in.</p> <p><strong>Training a model is <em>expensive</em>.</strong> It requires lots of very expensive hardware that consumes a significant amount of electricity. This makes RAG ideal for speeding up development at a lower cost. Developers can quickly update or change RAG data for information that changes rapidly, such as sports statistics, and it avoids the hassle of constantly training a model on new information.</p> <p>A very simple workflow for RAG would be something like this:</p> <ol> <li>Someone asks a question</li> <li>Search for relevant information in your RAG database</li> <li>Add the question and the RAG context to a prompt for the LLM</li> <li>Send the whole prompt to the LLM for inference</li> </ol> <p><strong>Step two is incredibly difficult.</strong></p> <p>If you are embarking on the RAG journey for the first time, there&rsquo;s a lot you need to know at a high level. Let&rsquo;s get started.</p> <h2 id="start-with-high-quality-documents" class="relative group">Start with high quality documents <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#start-with-high-quality-documents" aria-label="Anchor">#</a></span></h2><p>Imagine that you&rsquo;re one of the top chefs in the world. You work in a restaurant with <a href="https://en.wikipedia.org/wiki/Michelin_Guide" target="_blank" rel="noreferrer">Michelin stars</a>. The restaurant is opening for a special Saturday night dinner and your plan is to serve a delicious piece of fish for each guest. You already know how you plan to season the fish and how you&rsquo;re going to cook it. Your sauces are all ready.</p> <p>The fish arrives and when the cooler opens, you gasp. <em><strong>&ldquo;What is that smell?&rdquo;</strong></em> ๐Ÿ˜ฑ</p> <p><strong>What do you do?</strong> You have hungry guests on the way. Your sauce is exquisite and you know you can cook the fish perfectly. But how are you supposed to deal with fish that has gone bad along the way?</p> <p>This is likely the first step in your RAG journey: <strong>source document quality.</strong></p> <p>You might run into quality issues like these:</p> <ul> <li>Widely varying document structures or markup</li> <li>Documents written in other languages, or written in a language that isn&rsquo;t the author&rsquo;s primary language</li> <li>Large blocks of low readability text, such as kernel core dumps, command line output, or diagrams, that are difficult to parse</li> <li>Boilerplate language across multiple documents</li> <li>Metadata at the front of the document or scattered throughout</li> <li>No structure, markup, or boundaries whatsoever</li> <li>Incorrect, outdated, or problematic information</li> </ul> <p>This is a <strong>garbage in, garbage out</strong> problem. RAG can help you match documents to user questions, but if the documents steer the user in the wrong direction, the outcome is terrible.</p> <p>You have a few options:</p> <ul> <li>Engage with the groups who created or currently maintain the information to make improvements</li> <li>Use an LLM to summarize or extract information from the documents (some LLMs are good at building FAQs from documents)</li> <li>Find the highest quality documents in the group and start with those</li> </ul> <p>Once you have a document quality plan together, it&rsquo;s time for the next step.</p> <h2 id="getting-documents-ready-for-search" class="relative group">Getting documents ready for search <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#getting-documents-ready-for-search" aria-label="Anchor">#</a></span></h2><p>You have plenty of options at this step, but I&rsquo;ve had a good amount of luck with a hybrid search approach. This combines a vector (semantic) search along with a traditional full text search.</p> <p><strong>Vector searches aim to capture the <em>meaning</em> behind the search rather than just looking for keywords.</strong> They examine how words are positioned in a sentence and which words are closest together. These searches require a step where you convert a string of text into vectors and this can be time consuming on slower hardware.</p> <p><strong>Keyword, or full-text, searches are cheaper and easier to run.</strong> They&rsquo;re great for matching specific keywords or an exact phrase.</p> <p>When you combine both of these together, you get the best of both worlds.</p> <p>The challenge here is that you need an embedding model to convert strings into a list of vectors. Every embedding model, like an inference model, has a limit on how much text that it can turn into vectors in one shot. This is called the <em>context window</em> and it varies from model to model.</p> <p>If a model has a 350 token context window, that means it can only handle 350 tokens (close to 350 words) before the model overflows. If you put 450 tokens into this example model, it vectorizes the first 350 and skips the remaining 100. This means you can only do a vector search across the first 350 tokens.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p> <p>This is where chunking comes in. You need to split your documents into chunks so that they fit within the context window of the embedding model. However, there are advantages and disadvantages to larger or smaller chunks:</p> <ul> <li>Larger chunks preserve more of the text from your documents and make it easier to find document/chunk relationships. Your vector database is a little smaller and you can get better results from queries that are more broad.</li> <li>Smaller chunks give you a more precise retrieval and save you money at inference time since you&rsquo;re sending a little less context to the LLM. They&rsquo;re better for specific questions and they lower the risk of hallucination since you&rsquo;re providing context that is more specific.</li> </ul> <p>There are <em>plenty of options</em> to review here, especially with how you create chunks and set overlaps between the chunks. I really like where <a href="https://docs.unstructured.io/open-source/core-functionality/overview" target="_blank" rel="noreferrer">unstructured</a> is headed with their open source library. You can partition via different methods depending on the document type and then split within the partitions.</p> <p>This avoids situations where you might put a piece of chapter one with chapter two just because that&rsquo;s where the chunks happened to split. Partitioning on chapters first and then splitting into chunks keeps the relevant information together better.</p> <h2 id="time-to-search" class="relative group">Time to search <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#time-to-search" aria-label="Anchor">#</a></span></h2><p>I&rsquo;ve talked about hybrid searches already, but there&rsquo;s an <a href="https://pamelafox.github.io/my-py-talks/pgvector-python/" target="_blank" rel="noreferrer">excellent guide from Pamela Fox</a> that gives you a deep dive into RAG from &ldquo;I know nothing&rdquo; to &ldquo;I can do things!&rdquo; in 17 slides. This is a great way to visualize what is actually happening behind the scenes for each search type. Be prepared for calculus! ๐Ÿค“</p> <p>In a perfect world, your search results should:</p> <ul> <li>Return the smallest amount of matching chunks to avoid confusing the LLM (and to consume fewer tokens)</li> <li>Only provide chunks that are relevant to the user&rsquo;s question</li> <li>Contain all of the needed steps for a process (give all steps of a recipe instead of just the second half)</li> <li>Complete very quickly to avoid keeping the user waiting</li> </ul> <p>This can be tricky with certain languages, especially English. For example, if someone asks &ldquo;How do I keep a bat from flying away?&rdquo;, what are they talking about?</p> <ul> <li>Are they near a bat (the flying mammal) that they want to trap and keep from flying away? ๐Ÿฆ‡</li> <li>Are they playing a game of baseball and the bat keeps slipping from their hands as they swing? โšพ</li> </ul> <p>Vector searches help a lot with these confusing situations, but they&rsquo;re not perfect. Let&rsquo;s look at a way to improve them next.</p> <h2 id="refine-the-users-question" class="relative group">Refine the user&rsquo;s question <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#refine-the-users-question" aria-label="Anchor">#</a></span></h2><p>If you can budget some extra tokens to refine the user&rsquo;s question prior to searching, you can improve your RAG searches substantially.</p> <p>Let&rsquo;s keep the last example going and assume you have an AI service that handles baseball questions. In your case, you could take the user&rsquo;s question and clarify it using an LLM. Here&rsquo;s an example prompt (that is totally untested):</p> <pre tabindex="0"><code>You are a helpful AI assistant and an expert in the game of baseball. Use the question provided below to create five questions which are more specific and relevant to baseball terminology. Question: How do I keep a bat from flying away? </code></pre><p>The LLM might reply with something like this:</p> <pre tabindex="0"><code>1. How can I keep my hands tacky so the bat will stay in my hands when I swing? 2. How can I keep a better grip on a bat when my hands are sweaty? 3. What coverings or other materials can I add to a bat to get a better grip? 4. Can I adjust my swing to avoid losing my grip on the bat? 5. Are there exercises I can do to avoid letting the bat go when I swing? </code></pre><p>These refined questions offer more ways for vector searches to match document since there is more meaning to search through. LLMs often add more relevant keywords to the questions and that can be helpful for keyword searches, too.</p> <p>You can then adjust your workflow to something like this:</p> <ol> <li>Receive a question from the user</li> <li>Refine the question using an LLM</li> <li>Use the refined questions to search the RAG database</li> <li>Add the context and the <strong>original question</strong> to the prompt (we want to maintain the user&rsquo;s original intent!)</li> <li>Send the prompt to the LLM</li> </ol> <p>LLMs can hallucinate and make bad choices, so always send the user&rsquo;s original question to the LLM rather than the refined questions. If the LLM hallucinates on the question refinement step, the issue is contained to the RAG search rather than the RAG search and inference.</p> <h2 id="extra-credit" class="relative group">Extra credit <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#extra-credit" aria-label="Anchor">#</a></span></h2><p>There are plenty of ways to tweak this process depending on your time and budget.</p> <h3 id="categories" class="relative group">Categories <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#categories" aria-label="Anchor">#</a></span></h3><p>You can break your documents into categories and infer what the user is asking about to narrow your search. Imagine you needed to answer questions for all sports. You might break up your documents into categories for each sport and you could limit your searches to that sport. An LLM might be able to help you quickly determine which sport the user is asking about and then you can narrow your RAG search to only that sport.</p> <h3 id="get-the-whole-document" class="relative group">Get the whole document <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#get-the-whole-document" aria-label="Anchor">#</a></span></h3><p>Let&rsquo;s day you do a RAG hybrid search and your top 10 results brings back 7 chunks from the same document. In that case, that entire document or portion of the document is likely really useful for the user. You might want to build in some functionality that retrieves the whole document, or perhaps the whole chapter/section, in these situations and sends it to the LLM.</p> <h3 id="prioritize-documents" class="relative group">Prioritize documents <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#prioritize-documents" aria-label="Anchor">#</a></span></h3><p>Certain documents in your collection might have a higher priority over others. For example, if you have support teams that track which documents they refer customers to most often, those documents should be weighted higher in your results. You might be able to examine web traffic to tell you which articles on your site are accessed most often. Those documents would be great for a higher weight.</p> <h3 id="links-to-source" class="relative group">Links to source <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#links-to-source" aria-label="Anchor">#</a></span></h3><p>Document quality issues might lead you to put short summaries of documents in your RAG database. Adding a source link to the original page is helpful because an LLM would be able to answer the question at a high level and refer the user to a specific page for a deeper dive.</p> <h3 id="raft" class="relative group">RAFT <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#raft" aria-label="Anchor">#</a></span></h3><p>RAG and fine tuning (RAFT) is another good option if your budget allows for it. You can train your LLM on your high quality documents (quality is more important with RAFT) and provide additional documents via RAG when needed. For example, you might fine tune your model on sports data from last season and earlier. You could then provide the current season&rsquo;s data via RAG. This gives you great responses on historical data while allowing you to quickly update your current data with the latest games.</p> <h2 id="summary" class="relative group">Summary <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor">#</a></span></h2><p><strong>RAG isn&rsquo;t easy.</strong> ๐Ÿ˜‚</p> <p>Don&rsquo;t let anyone tell you it&rsquo;s easy. You would never take a stack of documents, cut them into pieces, throw them into a box, and put them in front of a brand new hire at your company. Don&rsquo;t try to do the same thing with RAG. RAG is a quick way to find all of the places in your organization where old data has been ignored. ๐Ÿคญ</p> <p>Start with high quality documents that have meaningful, relevant information for your users. Get them into a database where you can search them quickly and efficiently.</p> <p>Once they&rsquo;re in the database, get creative about how you search them. Simply returning relevant chunks is a good start. From there, look to see how you can take those results and expand upon them.</p> <p>Good luck in your adventure! ๐Ÿง—โ€โ™‚๏ธ</p> <div class="footnotes" role="doc-endnotes"> <hr> <ol> <li id="fn:1"> <p>Lots of hand-waving happening here. ๐Ÿ˜† Every model counts tokens differently and you may need to look for a specific tokenizer to know how many tokens can fit within the window.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> </ol> </div>Viewing Xorg logs with journalctl in Fedorahttps://major.io/p/xorg-logs-fedora-journalctl/Sun, 16 Feb 2025 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/xorg-logs-fedora-journalctl/<p>I love being an early adopter and trudging off into the unknown. After all, that&rsquo;s one of the best ways to learn new things and you end up improving the experience for everyone who comes behind you. However, things can get a little frustrating from time to time especially when your daily work dictates that your desktop works really well. ๐Ÿ˜‰</p> <p>Sway has been my desktop of choice for a few years and although it seems to work well, I ran into lots of issues with Wayland. It was easy to plot a course around most of these problems, but not all of them.</p> <p>I&rsquo;ve recently run back to safety with my old, trusty, i3 window manager in Xorg. Then I realized a few of my Xorg configuration weren&rsquo;t taking effect and I couldn&rsquo;t figure out how to isolate the Xorg logs in the system journal to narrow down the problem.</p> <p>Skip to the end if you&rsquo;re short on time or peruse the next section if you haven&rsquo;t been deep in the innards of your system journal in a while. ๐Ÿ”</p> <h2 id="journal-metadata" class="relative group">Journal metadata <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#journal-metadata" aria-label="Anchor">#</a></span></h2><p>Every journal entry in journald has metadata attached to it which you can use to filter the logs. Most people know about filtering based on systemd services, like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt; journalctl --boot --unit chronyd.service | head </span></span><span class="line"><span class="cl">systemd[1]: Starting chronyd.service - NTP client/server... </span></span><span class="line"><span class="cl">chronyd[2662]: chronyd version 4.6.1 starting (+CMDMON +NTP +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +ASYNCDNS +NTS +SECHASH +IPV6 +DEBUG) </span></span><span class="line"><span class="cl">chronyd[2662]: Using leap second list /usr/share/zoneinfo/leap-seconds.list </span></span><span class="line"><span class="cl">chronyd[2662]: Frequency -3.595 +/- 6.086 ppm read from /var/lib/chrony/drift </span></span><span class="line"><span class="cl">chronyd[2662]: Loaded seccomp filter (level 2) </span></span><span class="line"><span class="cl">systemd[1]: Started chronyd.service - NTP client/server. </span></span><span class="line"><span class="cl">chronyd[2662]: Added source 192.168.10.1 </span></span><span class="line"><span class="cl">chronyd[2662]: Selected source 208.67.72.50 (2.fedora.pool.ntp.org) </span></span><span class="line"><span class="cl">chronyd[2662]: System clock TAI offset set to 37 seconds </span></span><span class="line"><span class="cl">chronyd[2662]: Selected source 173.73.96.68 (2.fedora.pool.ntp.org) </span></span></code></pre></div><p>This command shows all of the messages from the <code>chronyd</code> service since the last boot. However, we can get much more specific with our filtering using other criteria.</p> <h2 id="examining-metadata" class="relative group">Examining metadata <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#examining-metadata" aria-label="Anchor">#</a></span></h2><p>You can examine the metadata behind each log line with the json output in journalctl:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt; journalctl --boot --unit chronyd.service -o json -n 1 | jq </span></span><span class="line"><span class="cl">{ </span></span><span class="line"><span class="cl"> &#34;_CMDLINE&#34;: &#34;/usr/sbin/chronyd -F 2&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_CGROUP&#34;: &#34;/system.slice/chronyd.service&#34;, </span></span><span class="line"><span class="cl"> &#34;_MACHINE_ID&#34;: &#34;xxxxx&#34;, </span></span><span class="line"><span class="cl"> &#34;_UID&#34;: &#34;990&#34;, </span></span><span class="line"><span class="cl"> &#34;SYSLOG_TIMESTAMP&#34;: &#34;Feb 16 13:55:59 &#34;, </span></span><span class="line"><span class="cl"> &#34;__SEQNUM_ID&#34;: &#34;c94633ee6da2480ca4602ca6ab47f82a&#34;, </span></span><span class="line"><span class="cl"> &#34;_PID&#34;: &#34;2662&#34;, </span></span><span class="line"><span class="cl"> &#34;PRIORITY&#34;: &#34;6&#34;, </span></span><span class="line"><span class="cl"> &#34;_HOSTNAME&#34;: &#34;zorro&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_SLICE&#34;: &#34;system.slice&#34;, </span></span><span class="line"><span class="cl"> &#34;SYSLOG_FACILITY&#34;: &#34;3&#34;, </span></span><span class="line"><span class="cl"> &#34;_GID&#34;: &#34;989&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_INVOCATION_ID&#34;: &#34;156fce8836374564b01aeb6628160ccb&#34;, </span></span><span class="line"><span class="cl"> &#34;__CURSOR&#34;: &#34;s=c94633ee6da2480ca4602ca6ab47f82a;i=19fb3a;b=ed9e1fccb1744136a3d726bbf2425388;m=33abfdf67;t=62e47cbf2b72f;x=f9a8d4e3bc4fef30&#34;, </span></span><span class="line"><span class="cl"> &#34;__MONOTONIC_TIMESTAMP&#34;: &#34;13870554983&#34;, </span></span><span class="line"><span class="cl"> &#34;_SOURCE_REALTIME_TIMESTAMP&#34;: &#34;1739735759501027&#34;, </span></span><span class="line"><span class="cl"> &#34;_TRANSPORT&#34;: &#34;syslog&#34;, </span></span><span class="line"><span class="cl"> &#34;_EXE&#34;: &#34;/usr/sbin/chronyd&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_UNIT&#34;: &#34;chronyd.service&#34;, </span></span><span class="line"><span class="cl"> &#34;SYSLOG_IDENTIFIER&#34;: &#34;chronyd&#34;, </span></span><span class="line"><span class="cl"> &#34;_BOOT_ID&#34;: &#34;ed9e1fccb1744136a3d726bbf2425388&#34;, </span></span><span class="line"><span class="cl"> &#34;__REALTIME_TIMESTAMP&#34;: &#34;1739735759501103&#34;, </span></span><span class="line"><span class="cl"> &#34;__SEQNUM&#34;: &#34;1702714&#34;, </span></span><span class="line"><span class="cl"> &#34;_RUNTIME_SCOPE&#34;: &#34;system&#34;, </span></span><span class="line"><span class="cl"> &#34;SYSLOG_PID&#34;: &#34;2662&#34;, </span></span><span class="line"><span class="cl"> &#34;_CAP_EFFECTIVE&#34;: &#34;2000400&#34;, </span></span><span class="line"><span class="cl"> &#34;MESSAGE&#34;: &#34;Selected source 173.73.96.68 (2.fedora.pool.ntp.org)&#34;, </span></span><span class="line"><span class="cl"> &#34;_COMM&#34;: &#34;chronyd&#34;, </span></span><span class="line"><span class="cl"> &#34;_SELINUX_CONTEXT&#34;: &#34;system_u:system_r:chronyd_t:s0&#34; </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><p>The most helpful one for us is <code>_COMM_</code>. We can use it to limit our search solely to Xorg logs.</p> <p>Every Xorg startup has a line with the Xorg version that looks like this: <code>X.Org X Server 1.21.1.15</code>. Let&rsquo;s search for that:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt; journalctl --boot -o json | grep -i &#34;x.org x server&#34; | jq </span></span><span class="line"><span class="cl">{ </span></span><span class="line"><span class="cl"> &#34;_HOSTNAME&#34;: &#34;zorro&#34;, </span></span><span class="line"><span class="cl"> &#34;_BOOT_ID&#34;: &#34;ed9e1fccb1744136a3d726bbf2425388&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_INVOCATION_ID&#34;: &#34;5cf933063b1246909d4ea15e7154bff4&#34;, </span></span><span class="line"><span class="cl"> &#34;_MACHINE_ID&#34;: &#34;xxxxx&#34;, </span></span><span class="line"><span class="cl"> &#34;__CURSOR&#34;: &#34;s=c94633ee6da2480ca4602ca6ab47f82a;i=19de06;b=ed9e1fccb1744136a3d726bbf2425388;m=4597347;t=62e2f4fcc855d;x=b4f482006e1523ff&#34;, </span></span><span class="line"><span class="cl"> &#34;__MONOTONIC_TIMESTAMP&#34;: &#34;72971079&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_SESSION&#34;: &#34;2&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_SLICE&#34;: &#34;user-1000.slice&#34;, </span></span><span class="line"><span class="cl"> &#34;_SELINUX_CONTEXT&#34;: &#34;system_u:system_r:xdm_t:s0-s0:c0.c1023&#34;, </span></span><span class="line"><span class="cl"> &#34;__SEQNUM_ID&#34;: &#34;c94633ee6da2480ca4602ca6ab47f82a&#34;, </span></span><span class="line"><span class="cl"> &#34;_AUDIT_LOGINUID&#34;: &#34;1000&#34;, </span></span><span class="line"><span class="cl"> &#34;__REALTIME_TIMESTAMP&#34;: &#34;1739630597408093&#34;, </span></span><span class="line"><span class="cl"> &#34;_CMDLINE&#34;: &#34;/usr/libexec/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3&#34;, </span></span><span class="line"><span class="cl"> &#34;_AUDIT_SESSION&#34;: &#34;2&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_USER_SLICE&#34;: &#34;-.slice&#34;, </span></span><span class="line"><span class="cl"> &#34;__SEQNUM&#34;: &#34;1695238&#34;, </span></span><span class="line"><span class="cl"> &#34;_GID&#34;: &#34;1000&#34;, </span></span><span class="line"><span class="cl"> &#34;_RUNTIME_SCOPE&#34;: &#34;system&#34;, </span></span><span class="line"><span class="cl"> &#34;SYSLOG_IDENTIFIER&#34;: &#34;/usr/libexec/gdm-x-session&#34;, </span></span><span class="line"><span class="cl"> &#34;MESSAGE&#34;: &#34;X.Org X Server 1.21.1.15&#34;, </span></span><span class="line"><span class="cl"> &#34;_STREAM_ID&#34;: &#34;7f35e3ce14d44dc8b589be76d4d355d9&#34;, </span></span><span class="line"><span class="cl"> &#34;_TRANSPORT&#34;: &#34;stdout&#34;, </span></span><span class="line"><span class="cl"> &#34;_CAP_EFFECTIVE&#34;: &#34;0&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_UNIT&#34;: &#34;session-2.scope&#34;, </span></span><span class="line"><span class="cl"> &#34;_UID&#34;: &#34;1000&#34;, </span></span><span class="line"><span class="cl"> &#34;_EXE&#34;: &#34;/usr/libexec/Xorg&#34;, </span></span><span class="line"><span class="cl"> &#34;PRIORITY&#34;: &#34;4&#34;, </span></span><span class="line"><span class="cl"> &#34;_COMM&#34;: &#34;Xorg&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_CGROUP&#34;: &#34;/user.slice/user-1000.slice/session-2.scope&#34;, </span></span><span class="line"><span class="cl"> &#34;_SYSTEMD_OWNER_UID&#34;: &#34;1000&#34;, </span></span><span class="line"><span class="cl"> &#34;_PID&#34;: &#34;3929&#34; </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><p>Note that the value for <code>_COMM</code> is <code>Xorg</code>. We can use that to search our logs with ease using the <code>cat</code> output from journalctl, which makes the output as terse as possible. It removes all the headers and make it look like you&rsquo;re reading a plain old text log file:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt; journalctl --output cat --boot _COMM=Xorg | head </span></span><span class="line"><span class="cl">(--) Log file renamed from &#34;/home/major/.local/share/xorg/Xorg.pid-3929.log&#34; to &#34;/home/major/.local/share/xorg/Xorg.0.log&#34; </span></span><span class="line"><span class="cl">X.Org X Server 1.21.1.15 </span></span><span class="line"><span class="cl">X Protocol Version 11, Revision 0 </span></span><span class="line"><span class="cl">Current Operating System: Linux zorro 6.12.13-200.fc41.x86_64 #1 SMP PREEMPT_DYNAMIC Sat Feb 8 20:05:26 UTC 2025 x86_64 </span></span><span class="line"><span class="cl">Kernel command line: BOOT_IMAGE=(hd0,gpt2)/vmlinuz-6.12.13-200.fc41.x86_64 root=UUID=bae22798-ce48-43e9-ac24-7bf7f7158e90 ro rootflags=subvol=root rd.luks.uuid=luks-defea11e-374c-48ab-83df-4f06c4c02186 rhgb quiet </span></span><span class="line"><span class="cl">Build ID: xorg-x11-server 21.1.15-1.fc41 </span></span><span class="line"><span class="cl">Current version of pixman: 0.44.2 </span></span><span class="line"><span class="cl"> Before reporting problems, check http://wiki.x.org </span></span><span class="line"><span class="cl"> to make sure that you have the latest version. </span></span><span class="line"><span class="cl">Markers: (--) probed, (**) from config file, (==) default setting, </span></span></code></pre></div><p>In my particular case, I was missing the amdgpu driver for Xorg. I installed the <code>xorg-x11-drv-amdgpu</code> package, rebooted, and now my logs showed the driver being loaded on startup:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt; journalctl --output cat --boot _COMM=Xorg | grep -i amdgpu | head </span></span><span class="line"><span class="cl">(II) Applying OutputClass &#34;AMDgpu&#34; to /dev/dri/card1 </span></span><span class="line"><span class="cl"> loading driver: amdgpu </span></span><span class="line"><span class="cl">(==) Matched amdgpu as autoconfigured driver 0 </span></span><span class="line"><span class="cl">(II) LoadModule: &#34;amdgpu&#34; </span></span><span class="line"><span class="cl">(II) Loading /usr/lib64/xorg/modules/drivers/amdgpu_drv.so </span></span><span class="line"><span class="cl">(II) Module amdgpu: vendor=&#34;X.Org Foundation&#34; </span></span><span class="line"><span class="cl">(II) AMDGPU: Driver for AMD Radeon: </span></span><span class="line"><span class="cl"> All GPUs supported by the amdgpu kernel driver </span></span><span class="line"><span class="cl">(II) AMDGPU(0): Creating default Display subsection in Screen section </span></span><span class="line"><span class="cl">(==) AMDGPU(0): Depth 24, (--) framebuffer bpp 32 </span></span></code></pre></div><h2 id="further-reading" class="relative group">Further reading <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#further-reading" aria-label="Anchor">#</a></span></h2><p>There are <em>tons</em> of ways to filter journald logs and one of the best resources for learning about all of them is the <a href="https://www.freedesktop.org/software/systemd/man/latest/journalctl.html" target="_blank" rel="noreferrer">journalctl man page</a>. There&rsquo;s also a <a href="https://gist.github.com/sergeyklay/f401dbc8286f732783e05072f03ecb61" target="_blank" rel="noreferrer">helpful journalctl cheat sheet</a> on GitHub.</p>Repairing 4Runner skid plate boltshttps://major.io/p/4runner-skid-plate-bolt-repair/Tue, 08 Oct 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/4runner-skid-plate-bolt-repair/<p>I replaced my old Toyota 4Runner with a new one so I could snag the last run of the 5th generation. It&rsquo;s a tough, reliable vehicle with just enough space for my family and our pets.</p> <p>However, this new one came with the same problem as my old one. All of the bolts that hold in the front skid plate were mostly stripped. Getting them out was difficult and I knew getting them back in would be worse.</p> <p>I&rsquo;ll cover how to fix it in this post. <strong>If you&rsquo;re in a hurry, scroll past the next section!</strong> Otherwise, let&rsquo;s get a little backstory.</p> <h2 id="backstory" class="relative group">Backstory <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#backstory" aria-label="Anchor">#</a></span></h2><p>All <a href="https://en.wikipedia.org/wiki/Toyota_4Runner" target="_blank" rel="noreferrer">4Runner models</a> are made in Japan in the <a href="https://en.wikipedia.org/wiki/Toyota_Motor_Corporation_Tahara_plant" target="_blank" rel="noreferrer">Tahara Plant</a>. The build quality is fantastic and you can tell that they&rsquo;re built with care. You can even find brief notes in Japanese inside the fenders or underneath the car where someone jotted some notes about something.</p> <p>After careful assembly in Japan, they head to various US ports where American workers add on any extra items that come with the trim level. That could include an upgraded exhaust, different wheels, or in my case, skid plates.</p> <p>The skid plate is a sturdy piece of steel that mounts under the front of the vehicle and protects lots of important components from damage. You can see it under the front bumper in this photo:</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="2048" height="1449" class="mx-auto my-0 rounded-md" alt="4runner-skid.jpg" loading="lazy" decoding="async" src="https://major.io/p/4runner-skid-plate-bolt-repair/4runner-skid_hu_8651f0c9f3a4f7c1.jpg" srcset="https://major.io/p/4runner-skid-plate-bolt-repair/4runner-skid_hu_e0b6e4056924ae06.jpg 330w,https://major.io/p/4runner-skid-plate-bolt-repair/4runner-skid_hu_8651f0c9f3a4f7c1.jpg 660w ,https://major.io/p/4runner-skid-plate-bolt-repair/4runner-skid_hu_94c625500a942ddc.jpg 1024w ,https://major.io/p/4runner-skid-plate-bolt-repair/4runner-skid_hu_b59d9bf49f151b44.jpg 1320w " sizes="100vw" /> </picture> <figcaption class="text-center">Skid plate on a white 4Runner</figcaption> </figure> </p> <p>If you&rsquo;ve ever owned a Toyota, you know that the factory is strict about torque applied to various bolts all over the vehicle. All of that gets thrown out the window when the workers at the US ports add on accessories.</p> <p>Based on all the complaints I&rsquo;ve seen across various 4Runner forums, they must use air wrenches or some kind of impact wrench to put on the bolts. If the bolt isn&rsquo;t in straight when they start, it destroys the bolt and causes problems with the mount holes. They also tighten the bolts <em>far past the acceptable torque specs.</em></p> <p>To make matters worse, if you take your car in for an oil change at most places, <strong>they&rsquo;ll have an impact wrench handy to ruin the bolts a bit more for you.</strong></p> <h2 id="root-cause" class="relative group">Root cause <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#root-cause" aria-label="Anchor">#</a></span></h2><p>If you have bolts that are getting stripped in the mount holes or they&rsquo;re getting stuck as you try to bring the bolts in or out, you likely have chunks of metal from the bolts wedged in the threads of the bolt holes.</p> <h2 id="ingredients" class="relative group">Ingredients <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ingredients" aria-label="Anchor">#</a></span></h2><p>The fix is quite cheap but very tedious. You&rsquo;ll need a few parts to get started:</p> <ul> <li> <p><a href="https://a.co/d/8EkxZxh" target="_blank" rel="noreferrer">Irwin Hanson 12002 T-Handle Tap Wrench (1/4&quot; to 1/2&quot;)</a>: This T-Handle wrench allows you to easily spin the tap screw to clear the metal fragments from your bolt holes. <strong>Don&rsquo;t use a socket set, drill, screwdriver, or anything powerful!</strong> You want to take this <em>slow</em>.</p> </li> <li> <p><a href="https://a.co/d/gy02vOu" target="_blank" rel="noreferrer">Irwin Tap 10-1 25mm Plug</a>: The bolts that go into the holes are M10 bolts with a 25mm thread, so this tap should fit perfectly.</p> </li> <li> <p><a href="https://a.co/d/grHmNR4" target="_blank" rel="noreferrer">Toyota part PT938-00140-AA</a>: This includes four new bolts with spacers and retaining washers to replace your stripped bolts.</p> </li> <li> <p><strong>14mm socket and socket wrench OR a 14mm wrench:</strong> You&rsquo;ll need this for removing the bolts and dealing with the hardware for the skid plate.</p> </li> <li> <p><strong>Some type of lubricant.</strong> I used WD-40, but don&rsquo;t tell anyone. People love to fight about whether WD-40 is a solvent, a grease, or a lubricant. ๐Ÿคทโ€โ™‚๏ธ</p> </li> </ul> <p>What&rsquo;s hilarious is that if you load the Amazon page for the Toyota part, it shows that everyone is buying tap screws and T-Handle wrenches:</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="720" height="301" class="mx-auto my-0 rounded-md" alt="buy-together.jpg" loading="lazy" decoding="async" src="https://major.io/p/4runner-skid-plate-bolt-repair/buy-together_hu_6aa39399c36a2baa.jpg" srcset="https://major.io/p/4runner-skid-plate-bolt-repair/buy-together_hu_766820810bf54acf.jpg 330w,https://major.io/p/4runner-skid-plate-bolt-repair/buy-together_hu_6aa39399c36a2baa.jpg 660w ,https://major.io/p/4runner-skid-plate-bolt-repair/buy-together.jpg 720w ,https://major.io/p/4runner-skid-plate-bolt-repair/buy-together.jpg 720w " sizes="100vw" /> </picture> <figcaption class="text-center">Even Amazon knows these bolts are a problem! ๐Ÿ˜†</figcaption> </figure> </p> <h2 id="fix-the-bolt-holes" class="relative group">Fix the bolt holes <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#fix-the-bolt-holes" aria-label="Anchor">#</a></span></h2><p>First things first, you&rsquo;ll need to get that skid plate off. <strong>I strongly recommend taking the rear bolts off first.</strong> If the bolts get stuck on the way out, take your time. I found that rocking in the other direction briefly and then trying to loosen them again seemed to work.</p> <p>With the rear bolts out, move to the bolts closest to the front of the car. Put a box or something sturdy underneath the skid plate that allows it to drop when it&rsquo;s loose but prevents it from knocking out one of your teeth when it falls. ๐Ÿค•</p> <p>When you loosen the front bolts, try loosening one of them 4-5 turns and then go to the other one. Keep going back and forth loosening the bolts until they loosen from the frame of the car. There are retaining washers on the top side of the bolt and removing those bolts aggressively will slide the retaining washers right off the bolt.</p> <p>Now you&rsquo;re ready to tap! ๐Ÿ‘</p> <p>Start in with the bolt holes in the rear and spray a decent amount of lubricant in the bottom and top of the bolt hole. Get your tap into the T-Handle wrench and slowly start turning it in the bolt hole like you were installing one of the bolts.</p> <p>๐Ÿ›‘ <strong>When you hit resistance, only go 1/2 to 1 turn further.</strong> Then back up 2-3 turns. This means you&rsquo;ve dislodged some metal fragments in the threads!</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="3072" height="4080" class="mx-auto my-0 rounded-md" alt="tapping.jpg" loading="lazy" decoding="async" src="https://major.io/p/4runner-skid-plate-bolt-repair/tapping_hu_7d3f917a9e638bcb.jpg" srcset="https://major.io/p/4runner-skid-plate-bolt-repair/tapping_hu_cb92a5a6c47260bb.jpg 330w,https://major.io/p/4runner-skid-plate-bolt-repair/tapping_hu_7d3f917a9e638bcb.jpg 660w ,https://major.io/p/4runner-skid-plate-bolt-repair/tapping_hu_ae7465d5a71826ed.jpg 1024w ,https://major.io/p/4runner-skid-plate-bolt-repair/tapping_hu_edbcbbcf9c6867c9.jpg 1320w " sizes="100vw" /> </picture> <figcaption class="text-center">Working on one of the back holes ๐Ÿ’ช</figcaption> </figure> </p> <p>After backing up a bit, keep screwing it in further until you hit more resistance. Only go 1/2 to 1 turn more, then back out 2-3 turns. Keep doing this until your tap shows up out of the top side of the bolt hole.</p> <p>With your tap sticking out of the top of the hole, grab a shop towel or paper towel and clear all of the metal filings away from the top of the hole. Then back the tap all the way out and clean your tap screw. It&rsquo;s likely going to be covered in black shavings.</p> <p>If you want to be really thorough, lubricate the hole once more and keep working the tap until the threads feel really smooth. I added some lubricant and fed a <strong>new bolt</strong> in through the top <strong>using finger strength only</strong> until I knew the threads were clear.</p> <p>You can test feed a <strong>new bolt</strong> through the bottom to verify that you&rsquo;ve done a good job. Don&rsquo;t use the old bolts for this. You&rsquo;ll just get more junk in the threads again. ๐Ÿ˜ญ</p> <p>Repeat these steps for the other four holes and you should be all set.</p> <h2 id="replace-the-skid-plate" class="relative group">Replace the skid plate <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#replace-the-skid-plate" aria-label="Anchor">#</a></span></h2><p>Be sure to discard the old bolts to avoid causing more problems for yourself. Re-assemble the new front bolts with the spacer and retaining washer just like they were when you took the skid plate down.</p> <p>Get the back bolts going in first and get them in about halfway. Start working on the front bolts after that.</p> <p>When all four bolts are in, grab your torque wrench and tighten them to <strong>22 ft/lbs or 30 Nm</strong> as specified in the <a href="trd_skid_plate_install_instructions.pdf">manual</a>:</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="667" height="387" class="mx-auto my-0 rounded-md" alt="skid-plate-torque.png" loading="lazy" decoding="async" src="https://major.io/p/4runner-skid-plate-bolt-repair/skid-plate-torque_hu_d154465ae73d83ea.png" srcset="https://major.io/p/4runner-skid-plate-bolt-repair/skid-plate-torque_hu_a98a38e5ccde5837.png 330w,https://major.io/p/4runner-skid-plate-bolt-repair/skid-plate-torque_hu_d154465ae73d83ea.png 660w ,https://major.io/p/4runner-skid-plate-bolt-repair/skid-plate-torque.png 667w ,https://major.io/p/4runner-skid-plate-bolt-repair/skid-plate-torque.png 667w " sizes="100vw" /> </picture> <figcaption class="text-center">Always check the instructions for torque specs! ๐Ÿ”ง</figcaption> </figure> </p> <p>If you don&rsquo;t own a torque wrench, I wouldn&rsquo;t tighten them much past finger tight with a socket wrench. Then drive to the hardware store and get a basic torque wrench. ๐Ÿ˜œ</p> <h2 id="prevention" class="relative group">Prevention <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#prevention" aria-label="Anchor">#</a></span></h2><p>This is easy but annoying: <strong>always remove the skid plate yourself before any kind of maintenance trip.</strong></p> <p>Yes, this sounds silly, but these mount points are finicky and there&rsquo;s no guarantee that the dreaded impact wrench will not show up again to ruin your bolts. I remove mine before any oil changes or scheduled maintenance at the dealer.</p> <p>Dealers have asked me in the past &ldquo;Where&rsquo;s your skid plate anyway?&rdquo; and I let them know I don&rsquo;t want my bolts stripped. ๐Ÿ˜…</p>Spell check in multiple languages with Firefoxhttps://major.io/p/firefox-multi-language-spell-check/Sun, 25 Aug 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/firefox-multi-language-spell-check/<p><strong>Bienvenidos!</strong><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> I&rsquo;ve been learning Spanish for just over a year and I often type messages in either Spanish or English (my native language) with coworkers and friends. Just like most people, I make spelling mistakes in both languages. ๐Ÿ™ƒ</p> <p>Firefox offers a feature for multi-language spell checking and translations but it can be a bit challenging to set up. This post explains how to load languages into Firefox and use them for spell checking.</p> <h2 id="installing-languages" class="relative group">Installing languages <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#installing-languages" aria-label="Anchor">#</a></span></h2><p>Take a trip over to [Dictionaries and Languages Packs] on Mozilla&rsquo;s site. Note that there are <strong>two columns</strong> available to you here:</p> <ul> <li><strong>Language packs</strong> give you the option to change your interface language to something different than your system&rsquo;s default language.</li> <li><strong>Dictionaries</strong> help with checking spelling.</li> </ul> <p>In the second column, click on the language you want to add for checking spelling. In my case, I picked the <a href="https://addons.mozilla.org/en-US/firefox/addon/diccionario-de-espa%C3%B1ol-espa%C3%B1a/" target="_blank" rel="noreferrer">Spanish (Spain) Dictionary</a> along with the <a href="https://addons.mozilla.org/en-US/firefox/addon/spanish-mexico-dictionary/" target="_blank" rel="noreferrer">Spanish (Mexico) Dictionary</a>. Install the dictionaries you want just like any other add-on!</p> <p>Go to the <code>about:addons</code> page in Firefox and you should see your languages under <strong>Languages</strong> and <strong>Dictionaries</strong> on the left side.</p> <h2 id="enable-the-language" class="relative group">Enable the language <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#enable-the-language" aria-label="Anchor">#</a></span></h2><p>Find an input field and right click inside the field. You should see a <strong>Languages</strong> context menu appear. Roll over that menu and a new menu pops out to the side:</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="502" height="502" class="mx-auto my-0 rounded-md" alt="context_menu.png" loading="lazy" decoding="async" src="https://major.io/p/firefox-multi-language-spell-check/contextmenu.png" /> </picture> <figcaption class="text-center">Firefox context menu showing multiple languages</figcaption> </figure> </p> <p>Click the checkbox to enable the languages that you want to use with the spell checker. That takes effect immediately!</p> <p>Gracias por leer hasta aquรญ!<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> ๐Ÿ˜œ</p> <div class="footnotes" role="doc-endnotes"> <hr> <ol> <li id="fn:1"> <p>Welcome!&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> <li id="fn:2"> <p>Thank you for reading this far.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> </ol> </div>My meeting hackshttps://major.io/p/meeting-hacks/Thu, 22 Aug 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/meeting-hacks/<p>Ask anyone about the toughest part of their workday and it usually comes down to one thing: meetings. There are plenty of reasons:</p> <ul> <li>The meeting could have been an email</li> <li>Nobody notices when I attend the meeting, but they notice when I don&rsquo;t</li> <li>The meeting is recurring whether there&rsquo;s something important to talk about or not</li> <li>There&rsquo;s no time for questions after everyone presents in a meeting</li> <li>Someone dominates the conversation</li> </ul> <p>This was a central problem in my <a href="https://txlf24-tech-career.major.io/#/" target="_blank" rel="noreferrer">&ldquo;Five tips for a thriving technology career&rdquo;</a> talk that I delivered this year. I wrote a <a href="https://major.io/p/texas-linux-fest-2024-recap/">recap</a> on the blog earlier this summer as well.</p> <p>I came up with some more ideas since then, so let&rsquo;s go!</p> <h2 id="use-headphones-or-earbuds" class="relative group">Use headphones or earbuds <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#use-headphones-or-earbuds" aria-label="Anchor">#</a></span></h2><p>I find it much easier to understand people in meetings when I have the audio closer to my ears. This helps a lot with understanding non-native English speakers or some native English speakers with thick accents. It reduces the noise from various things in my house (kids, pets, appliances) and allows me to focus on the small sounds that are important for understanding someone else.</p> <p>How many times have you been in a meeting with someone who talks constantly without earbuds or headphones and you can&rsquo;t break through with your own voice? Some computers will mute the incoming audio to avoid feedback sounds and you&rsquo;ll totally miss it when someone is trying to get your attention.</p> <p>I was once in a meeting where an attendee spoke at length about a topic that was already covered and multiple people tried to speak to let him know that he could stop. He was completely oblivious. The situation improved a lot recently with the addition of &ldquo;raised hands&rdquo; indicators in most meeting applications, but it&rsquo;s still not perfect.</p> <h2 id="background-music" class="relative group">Background music <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#background-music" aria-label="Anchor">#</a></span></h2><p>If you typically join meetings without earbuds or headphones, then this suggestion isn&rsquo;t for you. <strong>Also, you should go back and re-read the previous section.</strong> ๏ธ๐Ÿ˜œ</p> <p>Everyone has their own music preferences, but I find that playing some relaxing music at a low volume really helps me stay focused during meetings. I change the genre of music between different days depending on my mood. No matter what you choose, consider music without vocals to avoid distractions.</p> <p>A good place to start is Lofi Girl&rsquo;s &ldquo;beats to relax/study to&rdquo; playlist. You can listen on <a href="https://open.spotify.com/playlist/0vvXsWCC9xrXsKd4FyS8kM?si=dba7e37978e246bf" target="_blank" rel="noreferrer">Spotify</a> or on <a href="https://www.youtube.com/watch?v=jfKfPfyJRdk" target="_blank" rel="noreferrer">YouTube</a>. Very few songs have vocals, and if they do, it&rsquo;s barely noticeable. I&rsquo;ve found that I can keep this playlist on for hours without getting bored of it.</p> <p>This can be especially helpful for those marathon half or full day meetings. ๐Ÿ‘”</p> <h2 id="ask-about-taking-notes" class="relative group">Ask about taking notes <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ask-about-taking-notes" aria-label="Anchor">#</a></span></h2><p>Most meeting platforms offer transcription and audio/video recording already, but transcriptions are difficult to read and recordings aren&rsquo;t usually fun to watch. I love it when someone takes some concise notes about the points that were raised, who raised them, and who holds the action items to solve them.</p> <p><strong>If nobody&rsquo;s taking notes, ask if you can!</strong></p> <p>It&rsquo;s a great way to ensure you pay attention and the people who missed the meeting will thank you later. Nobody has turned me down yet when I&rsquo;ve asked.</p> <p>This can also be helpful if someone likes to talk over everyone else during the meeting or if someone birdwalks into other topics. Un-mute yourself and ask:</p> <blockquote> <p>Wait, are we still on that previous topic or have we moved to something else?</p></blockquote> <p>Another favorite question of mine is:</p> <blockquote> <p>Did we get an action item for that previous topic? Who owns that item?</p></blockquote> <p>Your note taking keeps speakers on track and ensures there is accountability and ownership for problems that need to be solved. It&rsquo;s also a great way to get your name in front of other people during larger meetings.</p> <h2 id="decline-that-meeting" class="relative group">Decline that meeting! <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#decline-that-meeting" aria-label="Anchor">#</a></span></h2><p>This one got the biggest reaction during my talk at Texas Linux Fest. Sometimes you just need to decline a meeting. Certain aspects of a meeting will push me to the &ldquo;No&rdquo; button faster than others, but here&rsquo;s my two biggest red flags:</p> <ul> <li> <p><strong>More than three attendees:</strong> It&rsquo;s difficult to get much done with a meeting that has 25 people in it. If someone sends me a calendar invitation unannounced and there are more than 3-5 people in the meeting, I ask them on Slack what I&rsquo;m expected to bring to the meeting. Often times, I hear <em>&ldquo;Oh, we wanted to be sure you were informed, but there are no action items for you.&rdquo;</em> That&rsquo;s a great time to say: <em>&ldquo;Can you send me the recording or the notes when it&rsquo;s over?&rdquo;</em></p> </li> <li> <p><strong>Missing agenda:</strong> If I&rsquo;m taking time out of my day to meet, I want to know about the meeting&rsquo;s goals. What should we have as we leave the meeting? Will we leave with a plan to do something? A set of decisions? Questions for another team?</p> </li> </ul> <p><strong>You are the only one that can advocate for your own time.</strong> Nobody else is going to do that for you<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. A very talented executive once told me:</p> <blockquote> <p>Time is the most valuable thing you bring to work every day. You can&rsquo;t get more of it, but you can waste it. Your experience and knowledge means nothing if you don&rsquo;t have time to use it. Treat your time as your most precious asset.</p></blockquote> <div class="footnotes" role="doc-endnotes"> <hr> <ol> <li id="fn:1"> <p>An administrative assistant can help but I&rsquo;ve never had one before. ๐Ÿ˜œ&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> </ol> </div>Rub some AI on ithttps://major.io/p/rub-some-ai-on-it/Wed, 21 Aug 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/rub-some-ai-on-it/<p><em>Author&rsquo;s note: This post is all about my personal thoughts on artificial intelligence (AI) and they don&rsquo;t represent the views of any employer or group.</em></p> <hr> <p>You can&rsquo;t escape the clutches of AI lately.</p> <p>It&rsquo;s in my smartphone <a href="https://support.google.com/messages/answer/14599070?hl=en" target="_blank" rel="noreferrer">nestled</a> next to my text messages. It&rsquo;s in my <a href="https://slack.com/features/ai" target="_blank" rel="noreferrer">work chats</a>. It&rsquo;s <a href="https://blog.duolingo.com/large-language-model-duolingo-lessons/" target="_blank" rel="noreferrer">reading my Spanish</a> in Duolingo. It&rsquo;s in my photo albums <a href="https://blog.adobe.com/en/publish/2023/04/18/new-adobe-lightroom-ai-innovations-empower-everyone-edit-like-pro" target="_blank" rel="noreferrer">retouching my images</a>.</p> <p>Sometimes we know that there&rsquo;s AI involved in something and sometimes we don&rsquo;t.</p> <p>However, it seems like so many are in a rush to implement some kind of AI offering without a full idea of why they&rsquo;re doing it. Here an excerpt from the <a href="https://hbr.org/2024/09/ai-wont-give-you-a-new-sustainable-advantage" target="_blank" rel="noreferrer">September 2024 issue</a> of Harvard Business Review that explains it well:</p> <blockquote> <p>Smart early movers in sectors adopting gen AI have certainly captured some of this value in the short term. But relatively soon all surviving companies in those sectors will have applied gen AI, and it wonโ€™t be a source of competitive advantage for any one of them, even where its impact on business and business practices will probably be profound. In fact, it will be more likely to remove a competitive advantage than to confer one. <strong>But hereโ€™s a silver lining: If you already have a competitive advantage that rivals cannot replicate using AI, the technology may serve to amplify the value you derive from that advantage.</strong></p></blockquote> <p>AI can help you only if:</p> <ol> <li>You have a product or service your customers value.</li> <li>You can leverage AI for specific improvements to that product or service that make it more valuable.</li> </ol> <h2 id="ai-is-not-valuable-alone" class="relative group">AI is not valuable alone <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ai-is-not-valuable-alone" aria-label="Anchor">#</a></span></h2><p>I&rsquo;m reminded of a time in the past where I was working hard on OpenStack public clouds. Kubernetes gained more traction day by day. Lots of customers told us: <em>&ldquo;I&rsquo;ve got to get on kubernetes so I can move faster.&rdquo;</em></p> <p>As we asked more about their challenges, they listed lots of things that should look familiar:</p> <ul> <li>Developers throwing code over the wall to Q/E and Q/E delays the release</li> <li>Software passes tests in development and staging, but fails miserably in production</li> <li>Monolithic applications were crushed by load spikes</li> <li>Operations teams struggled to deploy software efficiently and reliably</li> </ul> <p>They had a serious problem with delivering their software, but kubernetes couldn&rsquo;t make any of these better. <strong>Adding kubernetes would just give them two problems instead of one.</strong></p> <p>The running joke whenever someone ran into a problem with a server, a piece of code, or a service was to say <em>&ldquo;Why don&rsquo;t you rub a little kubernetes on it?&rdquo;</em> ๐Ÿคฃ</p> <p>I&rsquo;m seeing much of the same with AI as companies scramble to get their hands on the best hardware they can find and access to the highest quality large language models (LLMs) they can find. Cloud budgets are blown wide open. When someone asks about the AI effort, the reply is often: <em>&ldquo;We have to get it before our competitors do, or we&rsquo;re sunk!&rdquo;</em></p> <p>In February 2024, 36% of company earnings reports <a href="https://markets.businessinsider.com/news/stocks/ai-stocks-sp500-4q-tech-earnings-artificial-intelligence-goldman-sachs-2024-2?op=1" target="_blank" rel="noreferrer">mentioned AI</a> &ndash; a record high:</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="700" height="441" class="mx-auto my-0 rounded-md" alt="ai_mentions.webp" loading="lazy" decoding="async" src="https://major.io/p/rub-some-ai-on-it/ai_mentions_hu_7651662808290ed1.webp" srcset="https://major.io/p/rub-some-ai-on-it/ai_mentions_hu_47b5fda0bbee4736.webp 330w,https://major.io/p/rub-some-ai-on-it/ai_mentions_hu_7651662808290ed1.webp 660w ,https://major.io/p/rub-some-ai-on-it/ai_mentions.webp 700w ,https://major.io/p/rub-some-ai-on-it/ai_mentions.webp 700w " sizes="100vw" /> </picture> </figure> </p> <p>How many of them are actually doing something meaningful for their employees or customers with AI?</p> <h2 id="work-backwards-from-the-experience" class="relative group">Work backwards from the experience <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#work-backwards-from-the-experience" aria-label="Anchor">#</a></span></h2><p>One of my coworkers, Scott McCarty, wrote a great post on InfoWorld titled <a href="https://www.infoworld.com/article/3482087/what-generative-ai-can-do-for-sysadmins.html" target="_blank" rel="noreferrer">&ldquo;What generative AI can do for sysadmins&rdquo;</a>. What I love most about this article is that Scott remains laser-focused on the <em>experiences</em> and <em>challenges</em> that AI could improve.</p> <p>There are plenty of challenging situations that every sysadmin faces. The worst of these are when you&rsquo;re under incredible pressure to bring a system back into a working state and you need to pick through tons of information to identify the problem. You can sometimes spot these issues easily, such as a failing storage drive in a server. Other situations are much more difficult.</p> <p>The key is to <strong>start with the experience.</strong> Then work backwards from there.</p> <p>As an example, one pattern I often see is companies putting AI chatbots in front of their documentation. Sometimes the chatbot will help you find the right documentation faster, but sometimes it&rsquo;s not much better than a CTRL-F or a quick look at the documentation&rsquo;s table of contents.</p> <p>If your documentation is so complicated that you need to spend the time and money to put an AI chatbot in front of it, why not make your documentation better instead?</p> <p>When something does fail, why not put a link to the documentation in the log message itself? This pattern shows up a lot in modern software lately. If I try to enable a <a href="https://major.io/p/build-tailscale-exit-node-firewalld/">Tailscale exit node</a> but I haven&rsquo;t forwarded packets on an interface, I get quick instructions on the console with a link to documentation that explains it in more detail.</p> <p><strong>You cannot use AI to paper over a poor experience.</strong> Your customers will see right through it.</p> <h2 id="remember-the-human-side" class="relative group">Remember the human side <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#remember-the-human-side" aria-label="Anchor">#</a></span></h2><p>Sometimes companies simply try to take AI much too far and upset the human nature in all of us.</p> <p>A great example of this was Google&rsquo;s awful Olympics ad where it shows a girl&rsquo;s father using Google Gemini to <a href="https://www.cnn.com/2024/08/02/tech/google-olympics-ai-ad-artificial-intelligence/index.html" target="_blank" rel="noreferrer">write a letter to her hero</a>. The reaction at my house when we saw it was: <em>&ldquo;Wait, you&rsquo;re taking the time to write a letter to your hero and you&rsquo;re letting an AI write it? Could that be any more impersonal?&rdquo;</em> If I&rsquo;m writing a letter or email to someone I admire, I&rsquo;m taking the time to write it myself with my own voice.</p> <p>Another example is a Microsoft ad showing someone turning a long document into a long slide deck instead. If nobody wanted to read the document in the first place, why would they want to read your long slide deck? Also, how would they feel if they know you just jammed a document into a LLM to make a slide deck and then held them hostage in a conference room as you walked through a voiceless set of slides?</p> <p>This goes back to the last section, but if you&rsquo;re trying to add AI to replace a human interaction, think that through. Are you papering over a bad experience? Are you looking to cut costs without considering the customer reaction? What&rsquo;s your plan if the AI interactions backfire?</p> <h2 id="so-what-do-we-do" class="relative group">So what do we do? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#so-what-do-we-do" aria-label="Anchor">#</a></span></h2><p>If you work backwards from the customer experience and land on an LLM as the best way to solve a problem or enhance a product, that&rsquo;s great. However, the experience you are enabling should be so good that:</p> <ol> <li>Customers are genuinely delighted with the experience without knowing AI is involved</li> <li>You don&rsquo;t have to mention &ldquo;AI&rdquo; for the experience to feel innovative and delightful</li> <li>You have plans in place for when customers want more from the experience later</li> </ol> <p>Getting hardware or cloud infrastructure together and <a href="https://cfp.fedoraproject.org/flock-2024/talk/HM9Y8U/" target="_blank" rel="noreferrer">running an LLM on top is boring</a>. Even going retrieval augmented generation (RAG) is boring. We will soon live in a world where running an LLM is the same level of difficulty as running a web server or a container. That&rsquo;s not where the value lives.</p> <p>AI isn&rsquo;t the king. <strong>The experience is.</strong> If you forget that, you&rsquo;re just taking a problem and rubbing some AI on it.</p>AMD GPU missing from btophttps://major.io/p/amd-gpu-missing-btop/Tue, 20 Aug 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/amd-gpu-missing-btop/<p>I recently built a new PC for my birthday and I splurged a bit with a new AMD Radeon 7900 XTX GPU. Although I&rsquo;m not a heavy gamer, I&rsquo;m working with <a href="https://en.wikipedia.org/wiki/Large_language_model" target="_blank" rel="noreferrer">LLMs</a> more often and I&rsquo;m interested to do some of this work at home.</p> <p><a href="https://github.com/aristocratos/btop" target="_blank" rel="noreferrer">btop</a> is my go-to tracker for all kinds of data about my system, including CPU usage, memory usage, disk I/O, and network throughput. It&rsquo;s a great way to track down bottlenecks and find out why your CPU fan is spinning at max speed. ๐Ÿ˜œ</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="1908" height="1053" class="mx-auto my-0 rounded-md" alt="btop.png" loading="lazy" decoding="async" src="https://major.io/p/amd-gpu-missing-btop/btop_hu_9cc3191795d4c42.png" srcset="https://major.io/p/amd-gpu-missing-btop/btop_hu_a2ebb882e9f1f96b.png 330w,https://major.io/p/amd-gpu-missing-btop/btop_hu_9cc3191795d4c42.png 660w ,https://major.io/p/amd-gpu-missing-btop/btop_hu_f1b75f050de37643.png 1024w ,https://major.io/p/amd-gpu-missing-btop/btop_hu_19d3d13ca6b167c1.png 1320w " sizes="100vw" /> </picture> <figcaption class="text-center">Screenshot of btop running on my system</figcaption> </figure> </p> <h2 id="the-problem" class="relative group">The problem <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-problem" aria-label="Anchor">#</a></span></h2><p>My GPU wasn&rsquo;t showing up in btop after rebuilding the system. Normally, there&rsquo;s a bar for the GPU right underneath the CPU usage and it tracks the GPU usage as well as memory usage. Some cards report thermals there, too.</p> <p>The <a href="https://github.com/clbr/radeontop" target="_blank" rel="noreferrer">radeontop</a> tool worked fine and I can see the device in the hardware monitoring subsystem:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> cat /sys/class/hwmon/hwmon1/name </span></span><span class="line"><span class="cl"><span class="go">amdgpu </span></span></span></code></pre></div><h2 id="the-solution" class="relative group">The solution <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-solution" aria-label="Anchor">#</a></span></h2><p>I installed plenty of AMD packages, but I missed a critical one: <code>rocm-smi</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> dnf install rocm-smi </span></span><span class="line"><span class="cl"><span class="gp">&gt;</span> rocm-smi </span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">======================================== ROCm System Management Interface ======================================== </span></span></span><span class="line"><span class="cl"><span class="go">================================================== Concise Info ================================================== </span></span></span><span class="line"><span class="cl"><span class="go">Device Node IDs Temp Power Partitions SCLK MCLK Fan Perf PwrCap VRAM% GPU% </span></span></span><span class="line"><span class="cl"><span class="go"> (DID, GUID) (Edge) (Avg) (Mem, Compute, ID) </span></span></span><span class="line"><span class="cl"><span class="go">================================================================================================================== </span></span></span><span class="line"><span class="cl"><span class="go">0 1 0x744c, 55924 47.0ยฐC 21.0W N/A, N/A, 0 218Mhz 96Mhz 0% auto 327.0W 11% 10% </span></span></span><span class="line"><span class="cl"><span class="go">================================================================================================================== </span></span></span><span class="line"><span class="cl"><span class="go">============================================== End of ROCm SMI Log =============================================== </span></span></span></code></pre></div><p>That&rsquo;s the ticket! Now my btop data is complete.</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="526" height="308" class="mx-auto my-0 rounded-md" alt="btop-magnified.png" loading="lazy" decoding="async" src="https://major.io/p/amd-gpu-missing-btop/btop-magnified.png" /> </picture> <figcaption class="text-center">btop showing my GPU stats</figcaption> </figure> </p>Running ollama with an AMD Radeon 6600 XThttps://major.io/p/ollama-with-amd-radeon-6600xt/Thu, 08 Aug 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/ollama-with-amd-radeon-6600xt/<p>I&rsquo;m splitting time between two roles at work now and one of the roles has a heavy focus on <a href="https://en.wikipedia.org/wiki/Large_language_model" target="_blank" rel="noreferrer">LLMs</a>. Much like many of you, I&rsquo;ve given ChatGPT a try with questions from time to time. I&rsquo;ve also used GitHub Copilot within Visual Studio Code.</p> <p>They&rsquo;re all great, but I was really hoping to run something locally on my machine at home.</p> <p>Then I stumbled upon a great post on All Things Open titled &ldquo;<a href="https://allthingsopen.org/articles/build-a-local-ai-co-pilot" target="_blank" rel="noreferrer">Build a local AI co-pilot using IBM Granite Code, Ollama, and Continue</a>&rdquo; that started me down a path with <a href="https://ollama.com/" target="_blank" rel="noreferrer">ollama</a>. The ollama project gets you started with a local LLM and makes it easy to serve it for other applications to use.</p> <h2 id="its-so-slow-" class="relative group">It&rsquo;s so slow ๐ŸŒ <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#its-so-slow-" aria-label="Anchor">#</a></span></h2><p>When I first began connecting vscode to ollama, I noticed that the responses were incredibly slow. A quick check with <a href="https://github.com/aristocratos/btop" target="_blank" rel="noreferrer">btop</a> showed that my CPU was maxed out at 100% utilization and my GPU was entirely idle. That&rsquo;s not good.</p> <p>My first thought was to check the system journal with <code>sudo journalctl --boot -u ollama</code>. That gets me all the messages from ollama since I last booted the machine.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">source=images.go:781 msg=&#34;total blobs: 0&#34; </span></span><span class="line"><span class="cl">source=images.go:788 msg=&#34;total unused blobs removed: 0&#34; </span></span><span class="line"><span class="cl">source=routes.go:1155 msg=&#34;Listening on 127.0.0.1:11434 (version 0.3.4)&#34; </span></span><span class="line"><span class="cl">source=payload.go:30 msg=&#34;extracting embedded files&#34; dir=/tmp/ollama1586759388/runners </span></span><span class="line"><span class="cl">source=payload.go:44 msg=&#34;Dynamic LLM libraries [cpu_avx cpu_avx2 cuda_v11 rocm_v60102 cpu]&#34; </span></span><span class="line"><span class="cl">source=gpu.go:204 msg=&#34;looking for compatible GPUs&#34; </span></span><span class="line"><span class="cl">source=amd_linux.go:59 msg=&#34;ollama recommends running the https://www.amd.com/en/support/linux-drivers&#34; error=&#34;amdgpu version file missing: /sys/module/amdgpu/version stat /sys/module/amdgpu/version: no such file or directory&#34; </span></span><span class="line"><span class="cl">source=amd_linux.go:340 msg=&#34;amdgpu is not supported&#34; gpu=0 gpu_type=gfx1032 library=/usr/lib64 supported_types=&#34;[gfx1030 gfx1100 gfx1101 gfx1102]&#34; </span></span><span class="line"><span class="cl">source=amd_linux.go:342 msg=&#34;See https://github.com/ollama/ollama/blob/main/docs/gpu.md#overrides for HSA_OVERRIDE_GFX_VERSION usage&#34; </span></span><span class="line"><span class="cl">source=amd_linux.go:360 msg=&#34;no compatible amdgpu devices detected&#34; </span></span></code></pre></div><p>A couple of things in the output stood out to me:</p> <ul> <li><code>stat /sys/module/amdgpu/version: no such file or directory</code></li> <li><code>msg=&quot;amdgpu is not supported&quot; gpu=0 gpu_type=gfx1032 library=/usr/lib64 supported_types=&quot;[gfx1030 gfx1100 gfx1101 gfx1102]&quot;</code></li> <li><code>&quot;See https://github.com/ollama/ollama/blob/main/docs/gpu.md#overrides for HSA_OVERRIDE_GFX_VERSION usage&quot;</code></li> </ul> <p>Sure enough, the version was missing:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> stat /sys/module/amdgpu/version </span></span><span class="line"><span class="cl"><span class="go">stat: cannot statx &#39;/sys/module/amdgpu/version&#39;: No such file or directory </span></span></span></code></pre></div><p>And my AMD GPU is indeed an AMD Navi 23 chipset (gfx1032):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> lspci <span class="p">|</span> grep -i VGA </span></span><span class="line"><span class="cl"><span class="go">0f:00.0 VGA compatible controller: Advanced Micro Devices, Inc. [AMD/ATI] Navi 23 [Radeon RX 6600/6600 XT/6600M] (rev c7) </span></span></span></code></pre></div><p>I went over to the <a href="https://github.com/ollama/ollama/blob/main/docs/gpu.md#overrides" target="_blank" rel="noreferrer">linked overrides documentation</a> to figure out what <code>HSA_OVERRIDE_GFX_VERSION</code> is all about:</p> <blockquote> <p>Ollama leverages the AMD ROCm library, which does not support all AMD GPUs. In some cases you can force the system to try to use a similar LLVM target that is close. For example The Radeon RX 5400 is gfx1034 (also known as 10.3.4) however, ROCm does not currently support this target. The closest support is gfx1030. You can use the environment variable HSA_OVERRIDE_GFX_VERSION with x.y.z syntax. So for example, to force the system to run on the RX 5400, you would set HSA_OVERRIDE_GFX_VERSION=&ldquo;10.3.0&rdquo; as an environment variable for the server. If you have an unsupported AMD GPU you can experiment using the list of supported types below.</p></blockquote> <h2 id="the-fix" class="relative group">The fix <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-fix" aria-label="Anchor">#</a></span></h2><p>The docs recommended setting <code>HSA_OVERRIDE_GFX_VERSION=&quot;10.3.0&quot;</code> to see if my card will work. Let&rsquo;s edit the systemd unit file for ollama to drop in some additional configuration:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> sudo systemctl edit ollama.service </span></span></code></pre></div><p>An editor appeared with text in it:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1">### Editing /etc/systemd/system/ollama.service.d/override.conf</span> </span></span><span class="line"><span class="cl"><span class="c1">### Anything between here and the comment below will become the contents of the drop-in file</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">### Edits below this comment will be discarded</span> </span></span></code></pre></div><p>So I added the suggested override along with the path to my AMD ROCm directory:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1">### Editing /etc/systemd/system/ollama.service.d/override.conf</span> </span></span><span class="line"><span class="cl"><span class="c1">### Anything between here and the comment below will become the contents of the drop-in file</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">Environment</span><span class="o">=</span><span class="s">&#34;HSA_OVERRIDE_GFX_VERSION=10.3.0&#34;</span> </span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">&#34;ROCM_PATH=/opt/rocm&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">### Edits below this comment will be discarded</span> </span></span></code></pre></div><p>Then I can tell systemd to reload the unit and restart ollama:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> sudo systemctl daemon-reload </span></span><span class="line"><span class="cl"><span class="gp">&gt;</span> sudo systemctl stop ollama </span></span><span class="line"><span class="cl"><span class="gp">&gt;</span> sudo systemctl start ollama </span></span></code></pre></div><p>Back to the system journal for another look:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">source=amd_linux.go:348 msg=&#34;skipping rocm gfx compatibility check&#34; HSA_OVERRIDE_GFX_VERSION=10.3.0 </span></span><span class="line"><span class="cl">source=types.go:105 msg=&#34;inference compute&#34; id=0 library=rocm compute=gfx1032 driver=0.0 name=1002:73ff total=&#34;8.0 GiB&#34; available=&#34;5.9 GiB&#34; </span></span></code></pre></div><p>Success! ๐ŸŽ‰</p> <h2 id="giving-it-another-try" class="relative group">Giving it another try <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#giving-it-another-try" aria-label="Anchor">#</a></span></h2><p>I went back to vscode and tried some code completions, but they were only slightly faster than using the CPU. Each time I&rsquo;d wait for completion, I&rsquo;d watch btop and the GPU would spike, then the CPU, then the GPU spikes again, and so on.</p> <p>After talking with a coworker, it looks like my Radeon 6600 XT is great for games, but it lacks the RAM needed to load the model into the GPU. ๐Ÿ˜ญ From what I&rsquo;ve read, 24GB is the suggested minimum and that&rsquo;s the largest amount of RAM you&rsquo;ll find in most GeForce/Radeon consumer graphics cards.</p>Jellyfin fatal player errorhttps://major.io/p/jellyfin-fatal-player-error/Tue, 02 Jul 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/jellyfin-fatal-player-error/<p>Plex has been a mainstay for serving up media at home but it seems to have changed lately towards a more and more commercial offering. A friend recommended <a href="https://jellyfin.org/" target="_blank" rel="noreferrer">Jellyfin</a> and I deployed it on my Synology NAS in a Docker container.</p> <p>I did a few quick tests in a web browser and everything looked good. But then my Jellyfin android app told me:</p> <blockquote> <p>Playback failed due to a fatal player error</p></blockquote> <p>Everything looked fine in the browser, so it was time to dig in.</p> <h2 id="checking-the-logs" class="relative group">Checking the logs <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#checking-the-logs" aria-label="Anchor">#</a></span></h2><p>I opened up an ssh connection on the Synology to check the logs and found something unhelpful:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">Jellyfin.Api.Helpers.TranscodingJobHelper: FFmpeg exited with code 1 </span></span></code></pre></div><p>Running a few searches led me down rabbit holes to plenty of GitHub issues. None of them fixed the issue.</p> <h2 id="checking-the-browser-again" class="relative group">Checking the browser again <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#checking-the-browser-again" aria-label="Anchor">#</a></span></h2><p>I went through a few different videos from the Synology and played each. They all looked fine in Firefox until I reached one that seemed to stutter. The frame rate looked as if at least half of the frames were bring dropped.</p> <p>That particular video was in 4K with a high bit rate. Back on the synology, the CPU usage was through the roof.</p> <p>I configured graphics acceleration when I deployed Jellyfin. Perhaps it wasn&rsquo;t working?</p> <h2 id="jellyfin-deployment" class="relative group">Jellyfin deployment <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#jellyfin-deployment" aria-label="Anchor">#</a></span></h2><p>I deployed Jellyfin using the upstream guides with docker-compose:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jellyfin</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/jellyfin/jellyfin:latest</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jellyfin</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="m">1026</span><span class="p">:</span><span class="m">100</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;host&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">devices</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">/dev/dri</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># removed</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;unless-stopped&#34;</span><span class="w"> </span></span></span></code></pre></div><p>One of the GitHub issues I stumbled upon suggested being specific about the video devices that are mounted inside the container.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ls -al /dev/dri </span></span><span class="line"><span class="cl"><span class="go">total 0 </span></span></span><span class="line"><span class="cl"><span class="go">drwxr-xr-x 2 root root 80 Jun 10 20:23 . </span></span></span><span class="line"><span class="cl"><span class="go">drwxr-xr-x 12 root root 14140 Jun 10 20:24 .. </span></span></span><span class="line"><span class="cl"><span class="go">crw------- 1 root root 226, 0 Jun 10 20:24 card0 </span></span></span><span class="line"><span class="cl"><span class="go">crw-rw---- 1 root videodriver 226, 128 Jun 10 20:24 renderD128 </span></span></span></code></pre></div><p>I adjusted the deployment in <code>docker-compose.yaml</code> and tried again:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jellyfin</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/jellyfin/jellyfin:latest</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jellyfin</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="m">1026</span><span class="p">:</span><span class="m">100</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;host&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">devices</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">/dev/dri/renderD128:/dev/dri/renderD128</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">/dev/dri/card0:/dev/dri/card0</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># removed</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;unless-stopped&#34;</span><span class="w"> </span></span></span></code></pre></div><p>I redeployed jellyfin:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> docker-compose up -d jellyfin </span></span></code></pre></div><p>The Android app still had the fatal player error.</p> <h2 id="users-and-groups" class="relative group">Users and groups <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#users-and-groups" aria-label="Anchor">#</a></span></h2><p>Most of my Synology containers use the uid/gid pair of <code>1026:100</code> so allow them to read and write to my storage volume. The <code>/dev/dri/renderD128</code> is owned by the <code>videodriver</code> group:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> grep videodriver /etc/group </span></span><span class="line"><span class="cl"><span class="go">videodriver::937:PlexMediaServer </span></span></span></code></pre></div><p>This likely came from a time when I installed Plex on Synology via one of the Synology applications rather than from a container. <em>(I&rsquo;m not sure, but that&rsquo;s my guess.)</em></p> <p>I added that group to the container:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jellyfin</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/jellyfin/jellyfin:latest</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jellyfin</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="m">1026</span><span class="p">:</span><span class="m">100</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;host&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">group_add</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;937&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">devices</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">/dev/dri/renderD128:/dev/dri/renderD128</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">/dev/dri/card0:/dev/dri/card0</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># removed</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;unless-stopped&#34;</span><span class="w"> </span></span></span></code></pre></div><p>After redeploying the container, the Android app worked just fine! Also, the video stuttering disappeared when viewing the 4K video from the browser. ๐ŸŽ‰</p>Redirect local ports with firewalldhttps://major.io/p/firewalld-port-redirection/Fri, 28 Jun 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/firewalld-port-redirection/<p>Linux networking and firewalls give us plenty of options for redirecting traffic from one port to another. We can allow people outside our home to reach a web server we run in our internal network. That&rsquo;s called destination NAT, ot <a href="https://en.wikipedia.org/wiki/Network_address_translation#DNAT" target="_blank" rel="noreferrer">DNAT</a>.</p> <p>You can also redirect traffic to different ports on the same host. For example, if you have a daemon listening on port 3000, but you want people to reach that service on port 80, you can redirect traffic from 80 to 3000 on the same host (without network address translation).</p> <p>But how do we do this with <a href="https://firewalld.org/" target="_blank" rel="noreferrer">firewalld</a>? ๐Ÿค”</p> <h2 id="old-school-iptables-methods" class="relative group">Old-school iptables methods <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#old-school-iptables-methods" aria-label="Anchor">#</a></span></h2><p>Let&rsquo;s say you have a service running on port 3000 and you want to expose it to other computers on your same network as port 80. With iptables, you would typically start by enabling IP forwarding:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="go">sudo sysctl -w net.ipv4.ip_forward=1 </span></span></span></code></pre></div><p>Add two iptables rules to handle packets coming in from the outside as well as any locally generated packets:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">#</span> Handle locally-generated packets on the same machine. </span></span><span class="line"><span class="cl"><span class="go">sudo iptables -t nat -A PREROUTING -s 127.0.0.1 -p tcp --dport 80 -j REDIRECT --to 3000` </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="gp">#</span> Handle packets coming from outside the current machine. </span></span><span class="line"><span class="cl"><span class="go">sudo iptables -t nat -A OUTPUT -s 127.0.0.1 -p tcp --dport 80 -j REDIRECT --to 3000` </span></span></span></code></pre></div><p>There&rsquo;s a weird situation that happens on certain machines with certain network configurations where packets are not properly routed when they are destined for the local network adapter. To fix that, set one more sysctl configuration:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="go">sudo sysctl -w net.ipv4.conf.all.route_localnet=1 </span></span></span></code></pre></div><p>Remember to make these sysctl configurations permanent:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="go">sudo mkdir /etc/sysctl.conf.d/ </span></span></span><span class="line"><span class="cl"><span class="go">echo &#34;net.ipv4.ip_forward=1&#34; | sudo tee &gt;&gt; /etc/sysctl.conf.d/redirect.conf </span></span></span><span class="line"><span class="cl"><span class="go">echo &#34;net.ipv4.conf.all.route_localnet&#34; | sudo tee &gt;&gt; /etc/sysctl.conf.d/redirect.conf </span></span></span></code></pre></div><h2 id="why-consider-firewalld" class="relative group">Why consider firewalld? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#why-consider-firewalld" aria-label="Anchor">#</a></span></h2><p>I like firewalld because I can manage lots of settings for different firewall zones and allow access from one zone to another. It also allows me to put certain interfaces in trusted zones so they automatically get more access.</p> <p>Another nice aspect about firewalld is that it supports iptables and nftables backends. You don&rsquo;t have to think about the differences between the backends. All of that is taken care of for you.</p> <h2 id="port-redirections-in-firewalld" class="relative group">Port redirections in firewalld <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#port-redirections-in-firewalld" aria-label="Anchor">#</a></span></h2><p>Let&rsquo;s start by checking our default firewalld zone:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo firewall-cmd --list-all </span></span><span class="line"><span class="cl"><span class="go">FedoraServer (default, active) </span></span></span><span class="line"><span class="cl"><span class="go"> target: default </span></span></span><span class="line"><span class="cl"><span class="go"> ingress-priority: 0 </span></span></span><span class="line"><span class="cl"><span class="go"> egress-priority: 0 </span></span></span><span class="line"><span class="cl"><span class="go"> icmp-block-inversion: no </span></span></span><span class="line"><span class="cl"><span class="go"> interfaces: bond0 eno1 eno2 </span></span></span><span class="line"><span class="cl"><span class="go"> sources: </span></span></span><span class="line"><span class="cl"><span class="go"> services: dhcpv6-client http https </span></span></span><span class="line"><span class="cl"><span class="go"> ports: 51820/udp </span></span></span><span class="line"><span class="cl"><span class="go"> protocols: </span></span></span><span class="line"><span class="cl"><span class="go"> forward: yes </span></span></span><span class="line"><span class="cl"><span class="go"> masquerade: yes </span></span></span><span class="line"><span class="cl"><span class="go"> forward-ports: </span></span></span><span class="line"><span class="cl"><span class="go"> source-ports: </span></span></span><span class="line"><span class="cl"><span class="go"> icmp-blocks: </span></span></span><span class="line"><span class="cl"><span class="go"> rich rules: </span></span></span></code></pre></div><p>This output shows that my external network interfaces are attached to the zone and forwarding is already on in my case. If you see <code>forward: no</code> here, just run this command:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo firewall-cmd --add-forward </span></span><span class="line"><span class="cl"><span class="go">success </span></span></span></code></pre></div><p>Now firewalld will manage your <code>forwarding</code> sysctl variables for you on these interfaces. That&rsquo;s handy. ๐Ÿ˜‰</p> <p>Next, let&rsquo;s get the redirect working. We want to take external packets on port 80 and send them to 3000: on the local machine.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo firewall-cmd <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span><span class="go"> --add-forward-port=port=80:proto=tcp:toport=3000:toaddr=127.0.0.1 </span></span></span><span class="line"><span class="cl"><span class="go">success </span></span></span></code></pre></div><p>In this command, we told firewalld to take 80/tcp from the outside and send it to port 3000 on the local host (127.0.0.1). Let&rsquo;s double check our current configuration:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo firewall-cmd --list-all </span></span><span class="line"><span class="cl"><span class="go">FedoraServer (default, active) </span></span></span><span class="line"><span class="cl"><span class="go"> target: default </span></span></span><span class="line"><span class="cl"><span class="go"> ingress-priority: 0 </span></span></span><span class="line"><span class="cl"><span class="go"> egress-priority: 0 </span></span></span><span class="line"><span class="cl"><span class="go"> icmp-block-inversion: no </span></span></span><span class="line"><span class="cl"><span class="go"> interfaces: bond0 eno1 eno2 </span></span></span><span class="line"><span class="cl"><span class="go"> sources: </span></span></span><span class="line"><span class="cl"><span class="go"> services: dhcpv6-client http https </span></span></span><span class="line"><span class="cl"><span class="go"> ports: 51820/udp </span></span></span><span class="line"><span class="cl"><span class="go"> protocols: </span></span></span><span class="line"><span class="cl"><span class="go"> forward: yes </span></span></span><span class="line"><span class="cl"><span class="go"> masquerade: yes </span></span></span><span class="line"><span class="cl"><span class="go"> forward-ports: </span></span></span><span class="line"><span class="cl"><span class="go"> port=80:proto=tcp:toport=3000:toaddr=127.0.0.1 </span></span></span><span class="line"><span class="cl"><span class="go"> source-ports: </span></span></span><span class="line"><span class="cl"><span class="go"> icmp-blocks: </span></span></span><span class="line"><span class="cl"><span class="go"> rich rules: </span></span></span></code></pre></div><p>Test a connection to port 80 with <code>curl</code> and it should redirect to the service on port 3000.</p> <p>๐Ÿšจ <strong>If everything works, remember to save the firewalld configuration:</strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo firewall-cmd --runtime-to-permanent </span></span><span class="line"><span class="cl"><span class="go">success </span></span></span></code></pre></div><h2 id="extra-credit" class="relative group">Extra credit <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#extra-credit" aria-label="Anchor">#</a></span></h2><p>We can inspect the nftables rules to see the firewall rules that firewalld set for us. The <a href="https://wiki.archlinux.org/title/Nftables" target="_blank" rel="noreferrer">Arch Linux nftables wiki page</a> is superb for looking up those commands.</p> <p>If we dump the current ruleset, we see the rule we created in firewalld:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo nft list ruleset </span></span><span class="line"><span class="cl"><span class="go">---SNIP--- </span></span></span><span class="line"><span class="cl"><span class="go">chain nat_PRE_FedoraServer_allow { </span></span></span><span class="line"><span class="cl"><span class="go"> meta nfproto ipv4 tcp dport 80 dnat ip to 127.0.0.1:3000 </span></span></span><span class="line"><span class="cl"><span class="go">} </span></span></span><span class="line"><span class="cl"><span class="go">---SNIP--- </span></span></span></code></pre></div>amazon-ec2-utils in Fedorahttps://major.io/p/amazon-ec2-utils-fedora/Wed, 08 May 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/amazon-ec2-utils-fedora/<p>We&rsquo;ve all been in that situation where we see a device in Linux and wonder which physical device it corresponds to. I remember when I built my first NAS and received an alert that a drive had failed. It took me a while to figure out which physical drive actually needed to be replaced.</p> <p>This happens with network devices, too, and I <a href="https://major.io/p/understanding-systemds-predictable-network-device-names/">wrote a post</a> about systemd&rsquo;s predictable network device names back in 2015.</p> <p>Cloud instances often make it even more confusing because storage devices are fully virtualized and show up differently depending on the cloud provider. I recently packaged <a href="https://github.com/amazonlinux/amazon-ec2-utils" target="_blank" rel="noreferrer">amazon-ec2-utils</a> in Fedora to make this a little easier on AWS.</p> <h2 id="the-problem" class="relative group">The problem <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-problem" aria-label="Anchor">#</a></span></h2><p>I just built a test instance of Fedora 40 in AWS and the AWS API shows the block device mappings like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> aws ec2 describe-instances <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span><span class="go"> --instance-ids i-0687448a184ab0a9e | \ </span></span></span><span class="line"><span class="cl"><span class="go"> jq &#39;.Reservations[0].Instances[0].BlockDeviceMappings&#39; </span></span></span><span class="line"><span class="cl"><span class="go">[ </span></span></span><span class="line"><span class="cl"><span class="go"> { </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;DeviceName&#34;: &#34;/dev/sda1&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;Ebs&#34;: { </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;AttachTime&#34;: &#34;2024-05-08T15:24:03+00:00&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;DeleteOnTermination&#34;: true, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;Status&#34;: &#34;attached&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;VolumeId&#34;: &#34;vol-0832569729b6c5ea6&#34; </span></span></span><span class="line"><span class="cl"><span class="go"> } </span></span></span><span class="line"><span class="cl"><span class="go"> } </span></span></span><span class="line"><span class="cl"><span class="go">] </span></span></span></code></pre></div><p>However, if I check these devices inside the instance itself, I get something totally different:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">[fedora@f40 ~]$</span> sudo fdisk -l </span></span><span class="line"><span class="cl"><span class="go">Disk /dev/nvme0n1: 10 GiB, 10737418240 bytes, 20971520 sectors </span></span></span><span class="line"><span class="cl"><span class="go">Disk model: Amazon Elastic Block Store </span></span></span><span class="line"><span class="cl"><span class="go">Units: sectors of 1 * 512 = 512 bytes </span></span></span><span class="line"><span class="cl"><span class="go">Sector size (logical/physical): 512 bytes / 512 bytes </span></span></span><span class="line"><span class="cl"><span class="go">I/O size (minimum/optimal): 4096 bytes / 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go">Disklabel type: gpt </span></span></span><span class="line"><span class="cl"><span class="go">Disk identifier: 9FB58ED7-7581-4469-BEB7-64F069151EAF </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">Device Start End Sectors Size Type </span></span></span><span class="line"><span class="cl"><span class="go">/dev/nvme0n1p1 2048 206847 204800 100M EFI System </span></span></span><span class="line"><span class="cl"><span class="go">/dev/nvme0n1p2 206848 2254847 2048000 1000M Linux extended boot </span></span></span><span class="line"><span class="cl"><span class="go">/dev/nvme0n1p3 2254848 20971484 18716637 8.9G Linux root (ARM-64) </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">Disk /dev/zram0: 1.78 GiB, 1909456896 bytes, 466176 sectors </span></span></span><span class="line"><span class="cl"><span class="go">Units: sectors of 1 * 4096 = 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go">Sector size (logical/physical): 4096 bytes / 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go">I/O size (minimum/optimal): 4096 bytes / 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="gp">[fedora@f40 ~]$</span> ls -al /dev/sd* </span></span><span class="line"><span class="cl"><span class="go">ls: cannot access &#39;/dev/sd*&#39;: No such file or directory </span></span></span></code></pre></div><p>One disk isn&rsquo;t so bad, but what if we add more storage? The API tells me one thing:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">&gt;</span> aws ec2 describe-instances --instance-ids i-0687448a184ab0a9e <span class="p">|</span> jq <span class="s1">&#39;.Reservations[0].Instances[0].BlockDeviceMappings&#39;</span> </span></span><span class="line"><span class="cl"><span class="go">[ </span></span></span><span class="line"><span class="cl"><span class="go"> { </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;DeviceName&#34;: &#34;/dev/sda1&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;Ebs&#34;: { </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;AttachTime&#34;: &#34;2024-05-08T15:24:03+00:00&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;DeleteOnTermination&#34;: true, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;Status&#34;: &#34;attached&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;VolumeId&#34;: &#34;vol-0832569729b6c5ea6&#34; </span></span></span><span class="line"><span class="cl"><span class="go"> } </span></span></span><span class="line"><span class="cl"><span class="go"> }, </span></span></span><span class="line"><span class="cl"><span class="go"> { </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;DeviceName&#34;: &#34;/dev/sde&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;Ebs&#34;: { </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;AttachTime&#34;: &#34;2024-05-08T15:38:29.754000+00:00&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;DeleteOnTermination&#34;: false, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;Status&#34;: &#34;attached&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;VolumeId&#34;: &#34;vol-0a7ba05c5270d7aa3&#34;, </span></span></span><span class="line"><span class="cl"><span class="go"> &#34;VolumeOwnerId&#34;: &#34;xxx&#34; </span></span></span><span class="line"><span class="cl"><span class="go"> } </span></span></span><span class="line"><span class="cl"><span class="go"> } </span></span></span><span class="line"><span class="cl"><span class="go">] </span></span></span></code></pre></div><p>But then the instance tells me something else entirely:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">[fedora@f40 ~]$</span> sudo fdisk -l </span></span><span class="line"><span class="cl"><span class="go">Disk /dev/nvme0n1: 10 GiB, 10737418240 bytes, 20971520 sectors </span></span></span><span class="line"><span class="cl"><span class="go">Disk model: Amazon Elastic Block Store </span></span></span><span class="line"><span class="cl"><span class="go">Units: sectors of 1 * 512 = 512 bytes </span></span></span><span class="line"><span class="cl"><span class="go">Sector size (logical/physical): 512 bytes / 512 bytes </span></span></span><span class="line"><span class="cl"><span class="go">I/O size (minimum/optimal): 4096 bytes / 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go">Disklabel type: gpt </span></span></span><span class="line"><span class="cl"><span class="go">Disk identifier: 9FB58ED7-7581-4469-BEB7-64F069151EAF </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">Device Start End Sectors Size Type </span></span></span><span class="line"><span class="cl"><span class="go">/dev/nvme0n1p1 2048 206847 204800 100M EFI System </span></span></span><span class="line"><span class="cl"><span class="go">/dev/nvme0n1p2 206848 2254847 2048000 1000M Linux extended boot </span></span></span><span class="line"><span class="cl"><span class="go">/dev/nvme0n1p3 2254848 20971484 18716637 8.9G Linux root (ARM-64) </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">Disk /dev/zram0: 1.78 GiB, 1909456896 bytes, 466176 sectors </span></span></span><span class="line"><span class="cl"><span class="go">Units: sectors of 1 * 4096 = 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go">Sector size (logical/physical): 4096 bytes / 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go">I/O size (minimum/optimal): 4096 bytes / 4096 bytes </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">Disk /dev/nvme1n1: 10 GiB, 10737418240 bytes, 20971520 sectors </span></span></span><span class="line"><span class="cl"><span class="go">Disk model: Amazon Elastic Block Store </span></span></span><span class="line"><span class="cl"><span class="go">Units: sectors of 1 * 512 = 512 bytes </span></span></span><span class="line"><span class="cl"><span class="go">Sector size (logical/physical): 512 bytes / 512 bytes </span></span></span><span class="line"><span class="cl"><span class="go">I/O size (minimum/optimal): 4096 bytes / 4096 bytes </span></span></span></code></pre></div><h2 id="udev-rules-to-the-rescue" class="relative group">udev rules to the rescue <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#udev-rules-to-the-rescue" aria-label="Anchor">#</a></span></h2><p>The amazon-ec2-utils package provides some helpful udev rules and scripts to make it easier to identify these devices. This package is on the way to Fedora as I write this post, but it hasn&rsquo;t reached the stable repos yet. Once it does, you should be able to install it:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo dnf install amazon-ec2-utils </span></span></code></pre></div><p>In the meantime, you can download the latest build and install it on your instance:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo dnf install /usr/bin/koji </span></span><span class="line"><span class="cl"><span class="gp">$</span> koji download-build amazon-ec2-utils-2.2.0-2.fc40 </span></span><span class="line"><span class="cl"><span class="go">Downloading [1/2]: amazon-ec2-utils-2.2.0-2.fc40.src.rpm </span></span></span><span class="line"><span class="cl"><span class="go">[====================================] 100% 24.01 KiB / 24.01 KiB </span></span></span><span class="line"><span class="cl"><span class="go">Downloading [2/2]: amazon-ec2-utils-2.2.0-2.fc40.noarch.rpm </span></span></span><span class="line"><span class="cl"><span class="go">[====================================] 100% 20.53 KiB / 20.53 KiB </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="gp">$</span> sudo dnf install amazon-ec2-utils-2.2.0-2.fc40.noarch.rpm </span></span></code></pre></div><p>The cleanest method to get these new udev rules working is to reboot, but if you&rsquo;re in a hurry, there&rsquo;s an option to reload these rules without a reboot:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo udevadm control --reload-rules </span></span><span class="line"><span class="cl"><span class="gp">$</span> sudo udevadm trigger </span></span></code></pre></div><p>What do we have in <code>/dev/</code> now?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">[fedora@f40 ~]$</span> ls -al /dev/sd* </span></span><span class="line"><span class="cl"><span class="go">lrwxrwxrwx. 1 root root 7 May 8 15:44 /dev/sda1 -&gt; nvme0n1 </span></span></span><span class="line"><span class="cl"><span class="go">lrwxrwxrwx. 1 root root 9 May 8 15:44 /dev/sda11 -&gt; nvme0n1p1 </span></span></span><span class="line"><span class="cl"><span class="go">lrwxrwxrwx. 1 root root 9 May 8 15:44 /dev/sda12 -&gt; nvme0n1p2 </span></span></span><span class="line"><span class="cl"><span class="go">lrwxrwxrwx. 1 root root 9 May 8 15:44 /dev/sda13 -&gt; nvme0n1p3 </span></span></span><span class="line"><span class="cl"><span class="go">lrwxrwxrwx. 1 root root 7 May 8 15:44 /dev/sde -&gt; nvme1n1 </span></span></span></code></pre></div><p>We can put a filesystem down on the new device using the same name as the API presents:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo dnf install /usr/sbin/mkfs.btrfs </span></span><span class="line"><span class="cl"><span class="gp">$</span> sudo mkfs.btrfs /dev/sde </span></span><span class="line"><span class="cl"><span class="go">btrfs-progs v6.8.1 </span></span></span><span class="line"><span class="cl"><span class="go">See https://btrfs.readthedocs.io for more information. </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">Performing full device TRIM /dev/sde (10.00GiB) ... </span></span></span><span class="line"><span class="cl"><span class="go">NOTE: several default settings have changed in version 5.15, please make sure </span></span></span><span class="line"><span class="cl"><span class="go"> this does not affect your deployments: </span></span></span><span class="line"><span class="cl"><span class="go"> - DUP for metadata (-m dup) </span></span></span><span class="line"><span class="cl"><span class="go"> - enabled no-holes (-O no-holes) </span></span></span><span class="line"><span class="cl"><span class="go"> - enabled free-space-tree (-R free-space-tree) </span></span></span><span class="line"><span class="cl"><span class="go"></span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="go">Label: (null) </span></span></span><span class="line"><span class="cl"><span class="go">UUID: c2fb9e33-3bf6-4b5b-aa80-44e315f499de </span></span></span><span class="line"><span class="cl"><span class="go">Node size: 16384 </span></span></span><span class="line"><span class="cl"><span class="go">Sector size: 4096 (CPU page size: 4096) </span></span></span><span class="line"><span class="cl"><span class="go">Filesystem size: 10.00GiB </span></span></span><span class="line"><span class="cl"><span class="go">Block group profiles: </span></span></span><span class="line"><span class="cl"><span class="go"> Data: single 8.00MiB </span></span></span><span class="line"><span class="cl"><span class="go"> Metadata: DUP 256.00MiB </span></span></span><span class="line"><span class="cl"><span class="go"> System: DUP 8.00MiB </span></span></span><span class="line"><span class="cl"><span class="go">SSD detected: yes </span></span></span><span class="line"><span class="cl"><span class="go">Zoned device: no </span></span></span><span class="line"><span class="cl"><span class="go">Features: extref, skinny-metadata, no-holes, free-space-tree </span></span></span><span class="line"><span class="cl"><span class="go">Checksum: crc32c </span></span></span><span class="line"><span class="cl"><span class="go">Number of devices: 1 </span></span></span><span class="line"><span class="cl"><span class="go">Devices: </span></span></span><span class="line"><span class="cl"><span class="go"> ID SIZE PATH </span></span></span><span class="line"><span class="cl"><span class="go"> 1 10.00GiB /dev/sde </span></span></span></code></pre></div><p>Being able to know these device names during the instance launch or during storage operations makes it much easier to write automation for these devices. There&rsquo;s no guess work required to translate the device that an instance shows you to what you see via the API.</p>Fix big cursors in Java applications in Waylandhttps://major.io/p/java-big-cursors-wayland/Fri, 26 Apr 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/java-big-cursors-wayland/<p>Scroll through the list of <a href="https://major.io/tags/wayland/">Wayland posts</a> posts on the blog and you&rsquo;ll see that I&rsquo;ve solved plenty of weird problems with Wayland and the <a href="https://swaywm.org/" target="_blank" rel="noreferrer">Sway</a> compositor. Most are pretty easy to fix but some are a bit trickier.</p> <p>Java applications are notoriously unpredictable and Wayland takes unpredictability to the next level. One particular application on my desktop always seems to start with massive cursors.</p> <p>This post is about how I fixed and then discovered something interesting along the way.</p> <h2 id="fixing-big-cursors" class="relative group">Fixing big cursors <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#fixing-big-cursors" aria-label="Anchor">#</a></span></h2><p>I recently moved some investment and trading accounts from TD Ameritrade to <a href="https://tastytrade.com/" target="_blank" rel="noreferrer">Tastytrade</a>. Both offer Java applications that make trading easier, but Tastytrade&rsquo;s application always started with massive cursors.</p> <p>To make matters worse, sometimes the cursor looked lined up on the screen but then the click landed on the wrong buttons in the application! Errors are annoying. Errors that cost you money and time must be fixed. ๐Ÿ˜œ</p> <p>Some web searches eventually led me to Arch Linux&rsquo;s excellent <a href="https://wiki.archlinux.org/title/Wayland" target="_blank" rel="noreferrer">Wayland wiki page</a>. None of the adjustments or environment variables there had any effect on my cursors.</p> <p>I eventually landed on a page that suggested setting <code>XCURSOR_SIZE</code>. I don&rsquo;t remember ever setting that, but it was being set by <em>something</em>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> <span class="nb">echo</span> <span class="nv">$XCURSOR_SIZE</span> </span></span><span class="line"><span class="cl"><span class="go">24 </span></span></span></code></pre></div><p>One of the suggestions was to decrease it, so I decided to give <code>20</code> a try. That was too big, but <code>16</code> was perfect and it matched all of my other applications:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> <span class="nb">export</span> <span class="nv">XCURSOR_SIZE</span><span class="o">=</span><span class="m">20</span> </span></span><span class="line"><span class="cl"><span class="gp">#</span> /opt/tastytrade/bin/tastytrade </span></span></code></pre></div><p>That works fine when I start my application via the terminal, but how do I set it for the application when I start it from ulauncher in sway? ๐Ÿค”</p> <h2 id="desktop-file" class="relative group">Desktop file <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#desktop-file" aria-label="Anchor">#</a></span></h2><p>The Tastytade RPM comes with a <code>.desktop</code> file for launching the application. I copied that over to my local applications directory:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cp /opt/tastytrade/lib/tastytrade-tastytrade.desktop <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> ~/.local/share/applications/ </span></span></code></pre></div><p>Then I opened the copied <code>~/.local/share/applications/tastytrade-tastytrade.desktop</code> file in a text editor:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Desktop Entry]</span> </span></span><span class="line"><span class="cl"><span class="na">Name</span><span class="o">=</span><span class="s">tastytrade</span> </span></span><span class="line"><span class="cl"><span class="na">Comment</span><span class="o">=</span><span class="s">tastytrade</span> </span></span><span class="line"><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">/opt/tastytrade/bin/tastytrade</span> </span></span><span class="line"><span class="cl"><span class="na">Icon</span><span class="o">=</span><span class="s">/opt/tastytrade/lib/tastytrade.png</span> </span></span><span class="line"><span class="cl"><span class="na">Terminal</span><span class="o">=</span><span class="s">false</span> </span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">Application</span> </span></span><span class="line"><span class="cl"><span class="na">Categories</span><span class="o">=</span><span class="s">tastyworks</span> </span></span><span class="line"><span class="cl"><span class="na">MimeType</span><span class="o">=</span> </span></span></code></pre></div><p>I changed the <code>Exec</code> line to be:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">env XCURSOR_SIZE=16 /opt/tastytrade/bin/tastytrade</span> </span></span></code></pre></div><p>I launched the application again after making that change, but the cursors were still huge! There has to be another way. ๐Ÿค”</p> <h2 id="systemd-does-everything-" class="relative group">systemd does everything ๐Ÿ˜† <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#systemd-does-everything-" aria-label="Anchor">#</a></span></h2><p>After more searching and digging, I discovered that systemd has a capability to <a href="https://www.freedesktop.org/software/systemd/man/latest/environment.d.html" target="_blank" rel="noreferrer">set environment variables for user sessions</a>:</p> <blockquote> <p>Configuration files in the environment.d/ directories contain lists of environment variable assignments passed to services started by the systemd user instance. systemd-environment-d-generator(8) parses them and updates the environment exported by the systemd user instance. See below for an discussion of which processes inherit those variables.</p> <p>It is recommended to use numerical prefixes for file names to simplify ordering.</p> <p>For backwards compatibility, a symlink to /etc/environment is installed, so this file is also parsed.</p></blockquote> <p>Let&rsquo;s give that a try:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> mkdir -p ~/.config/environment.d/ </span></span><span class="line"><span class="cl"><span class="gp">$</span> vim ~/.config/environment.d/wayland.conf </span></span></code></pre></div><p>In the file, I added one line with a comment (because you will soon forget why you added it ๐Ÿ˜„):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Fix big cursors in Java apps in Wayland</span> </span></span><span class="line"><span class="cl"><span class="nv">XCURSOR_SIZE</span><span class="o">=</span><span class="m">16</span> </span></span></code></pre></div><p><strong>After a reboot, I launched my Java application and boom &ndash; the cursors were perfect!</strong> ๐ŸŽ‰</p> <p>I went back and cleaned up some other hacks I had applied and added them to that <code>wayland.conf</code> file:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># This was important at some point but I&#39;m afraid to remove it.</span> </span></span><span class="line"><span class="cl"><span class="c1"># Note to self: make detailed comments when adding lines here.</span> </span></span><span class="line"><span class="cl"><span class="nv">SDL_VIDEODRIVER</span><span class="o">=</span>wayland </span></span><span class="line"><span class="cl"><span class="nv">QT_QPA_PLATFORM</span><span class="o">=</span>wayland </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Reduce window decorations for VLC</span> </span></span><span class="line"><span class="cl"><span class="nv">QT_WAYLAND_DISABLE_WINDOWDECORATION</span><span class="o">=</span><span class="s2">&#34;1&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Fix weird window handling when Java apps do certain pop-ups</span> </span></span><span class="line"><span class="cl"><span class="nv">_JAVA_AWT_WM_NONREPARENTING</span><span class="o">=</span><span class="m">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Ensure Firefox is using Wayland code (not needed any more)</span> </span></span><span class="line"><span class="cl"><span class="nv">MOZ_ENABLE_WAYLAND</span><span class="o">=</span><span class="m">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Disable HiDPI</span> </span></span><span class="line"><span class="cl"><span class="nv">GDK_SCALE</span><span class="o">=</span><span class="m">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Fix big cursors in Java apps in Wayland</span> </span></span><span class="line"><span class="cl"><span class="nv">XCURSOR_SIZE</span><span class="o">=</span><span class="m">16</span> </span></span></code></pre></div><p>I&rsquo;m told there are some caveats with this solution, especially if your Wayland desktop doesn&rsquo;t use systemd to start. This is working for me with GDM launching Sway on Fedora 40.</p>cloud-init and dhcpcdhttps://major.io/p/fedora-cloud-init-dhcpcd/Thu, 18 Apr 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/fedora-cloud-init-dhcpcd/<p>We&rsquo;re all familiar with the trusty old <code>dhclient</code> on our Linux systems, but <a href="https://github.com/isc-projects/dhcp" target="_blank" rel="noreferrer">it went end-of-life in 2022</a>:</p> <pre tabindex="0"><code>NOTE: This software is now End-Of-Life. 4.4.3 is the final release planned. We will continue to keep the public issue tracker and user mailing list open. You should read this file carefully before trying to install or use the ISC DHCP Distribution. </code></pre><p>Most Linux distributions use <code>dhclient</code> along with cloud-init for the initial dhcp request during the first part of cloud-init&rsquo;s work. I set off to switch Fedora&rsquo;s cloud-init package to <code>dhcpcd</code> instead.</p> <h2 id="whats-new-with-dhcpcd" class="relative group">What&rsquo;s new with dhcpcd? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#whats-new-with-dhcpcd" aria-label="Anchor">#</a></span></h2><p>There are some nice things about <code>dhcpcd</code> that you can find in the <a href="https://github.com/NetworkConfiguration/dhcpcd" target="_blank" rel="noreferrer">GitHub repository</a>:</p> <ul> <li>Very small footprint with almost no dependencies on Fedora</li> <li>It can do DHCP and DHCPv6</li> <li>It can also be a <a href="https://en.wikipedia.org/wiki/Zeroconf" target="_blank" rel="noreferrer">ZeroConf</a> client</li> </ul> <p>The project had its last release back in December 2023 and had commits as recently as this week.</p> <h2 id="but-i-use-networkmanager" class="relative group">But I use NetworkManager <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#but-i-use-networkmanager" aria-label="Anchor">#</a></span></h2><p>That&rsquo;s great! A switch from <code>dhclient</code> to <code>dhcpcd</code> for cloud-init won&rsquo;t affect you.</p> <p>When cloud-init starts, it does an initial dhcp request to get just enough networking to reach the cloud&rsquo;s metadata service. This service provides all kinds of information for cloud-init, including network setup instructions and initial scripts to run.</p> <p>NetworkManager doesn&rsquo;t start taking action until cloud-init has written the network configuration to the system.</p> <h2 id="but-i-use-systemd-networkd" class="relative group">But I use systemd-networkd <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#but-i-use-systemd-networkd" aria-label="Anchor">#</a></span></h2><p>Same as with NetworkManager, this change applies to the <em>very</em> early boot and you won&rsquo;t notice a different when deploying new cloud systems.</p> <h2 id="how-can-i-get-it-right-now" class="relative group">How can I get it right now? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#how-can-i-get-it-right-now" aria-label="Anchor">#</a></span></h2><p>If you&rsquo;re using a recent build of Fedora rawhide (the unstable release under development), you likely have it right now on your cloud instance. Just run <code>journalctl --boot</code>, search for <code>dhcpcd</code>, and you should see these lines:</p> <pre tabindex="0"><code>cloud-init[725]: Cloud-init v. 24.1.4 running &#39;init-local&#39; at Wed, 17 Apr 2024 14:39:36 +0000. Up 6.13 seconds. dhcpcd[727]: dhcpcd-10.0.6 starting kernel: 8021q: 802.1Q VLAN Support v1.8 dhcpcd[730]: DUID 00:01:00:01:2d:b2:9b:a9:06:eb:18:e7:22:dd dhcpcd[730]: eth0: IAID 18:e7:22:dd dhcpcd[730]: eth0: soliciting a DHCP lease dhcpcd[730]: eth0: offered 172.31.26.195 from 172.31.16.1 dhcpcd[730]: eth0: leased 172.31.26.195 for 3600 seconds avahi-daemon[706]: Joining mDNS multicast group on interface eth0.IPv4 with address 172.31.26.195. avahi-daemon[706]: New relevant interface eth0.IPv4 for mDNS. avahi-daemon[706]: Registering new address record for 172.31.26.195 on eth0.IPv4. dhcpcd[730]: eth0: adding route to 172.31.16.0/20 dhcpcd[730]: eth0: adding default route via 172.31.16.1 dhcpcd[730]: control command: /usr/sbin/dhcpcd --dumplease --ipv4only eth0 </code></pre><p>There&rsquo;s also an <a href="https://bodhi.fedoraproject.org/updates/FEDORA-2024-51d7f6b005" target="_blank" rel="noreferrer">update pending for Fedora 40</a>, but it&rsquo;s currently held up by the beta freeze. That should appear as an update as soon as Fedora 40 is released.</p> <p>Keep in mind that if you have a system deployed already, cloud-init won&rsquo;t need to run again. Updating to Fedora 40 will update your cloud-init and pull in <code>dhcpcd</code>, but it won&rsquo;t need to run again since your configuration is already set.</p>Texas Linux Fest 2024 recap ๐Ÿค https://major.io/p/texas-linux-fest-2024-recap/Tue, 16 Apr 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/texas-linux-fest-2024-recap/<p>The 2024 <a href="https://2024.texaslinuxfest.org/" target="_blank" rel="noreferrer">Texas Linux Festival</a> just ended last weekend and it was a fun event as always. It&rsquo;s one my favorite events to attend because it&rsquo;s really casual. You have plenty of opportunities to see old friends, meet new people, and learn a few things along the way.</p> <p>I was fortunate enough to have two talks accepted for this year&rsquo;s event. One was focused on containers while the other was a (very belated) addition to my <a href="https://major.io/p/impostor-syndrome-talk-faqs-and-follow-ups/">impostor syndrome talk</a> from 2015.</p> <p>This was also my first time building slides with <a href="https://github.com/webpro/reveal-md" target="_blank" rel="noreferrer">reveal-md</a>, a &ldquo;batteries included&rdquo; package for making <a href="https://revealjs.com/" target="_blank" rel="noreferrer">reveal.js</a> slides. Nothing broke too badly and that was a relief.</p> <h2 id="containers-talk" class="relative group">Containers talk <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#containers-talk" aria-label="Anchor">#</a></span></h2><p>I&rsquo;ve wanted to share more of what I&rsquo;ve done with CoreOS in low-budget container deployments and this seemed like a good time to share it with the world out loud. My talk, <a href="https://txlf24-containers.major.io/#/" target="_blank" rel="noreferrer">Automated container updates with GitHub and CoreOS</a>, walked the audience through how to deploy containers on CoreOS, keep them updated, and update the container image source.</p> <p>My goal was to keep it as low on budget as possible. Much of it was centered around a stack of <a href="https://major.io/p/caddy-porkbun/">caddy</a>, librespeed, and docker-compose. All of it was kept up to date with <a href="https://major.io/p/watchtower/">watchtower</a>.</p> <p>My custom Caddy container needed support for <a href="https://porkbun.com/" target="_blank" rel="noreferrer">Porkbun&rsquo;s</a> DNS API and I used GitHub Actions to build that container and serve it to the internet using GitHub&rsquo;s package hosting. <em>This also gave me the opportunity to share how awesome Porkbun is for registering domains, including their <a href="https://porkbun.com/tld/jobs" target="_blank" rel="noreferrer">customized pig artwork</a> for every TLD imaginable.</em> ๐Ÿท</p> <p>We had a great discussion afterwards about how CoreOS <strong>does indeed live on</strong> as <a href="https://fedoraproject.org/coreos/" target="_blank" rel="noreferrer">Fedora CoreOS</a>.</p> <h2 id="tech-career-talk" class="relative group">Tech career talk <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#tech-career-talk" aria-label="Anchor">#</a></span></h2><p>This talk made me nervous because it had a lot of slides to cover, but I also wanted to leave plenty of time for questions. <a href="https://txlf24-tech-career.major.io/#/" target="_blank" rel="noreferrer">Five tips for a thriving technology career</a> built upon my old impostor syndrome talk by sharing some of the things I&rsquo;ve learned over the year that helped me succeed in my career.</p> <p>I managed to end early with time for questions, and boy did the audience have questions! ๐Ÿ“ฃ Some audience members helped me answer some questions, too!</p> <p>We talked a lot about office politics, tribal knowledge, and toxic workplaces. The audience generally agreed that most businesses tried to rub copious amounts of Confluence on their tribal knowledge problem, but it never improved. ๐Ÿ˜œ</p> <p>The room was full with people standing in the back and I&rsquo;m tremendously humbled by everyone who came. I received plenty of feedback afterwards and that&rsquo;s the best gift I could ever get. ๐ŸŽ</p> <h2 id="other-talks" class="relative group">Other talks <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#other-talks" aria-label="Anchor">#</a></span></h2><p><a href="https://github.com/anitazha" target="_blank" rel="noreferrer">Anita Zhang</a> had an excellent keynote talk on the second day about her unusual path into the world of technology. Her slides were pictures of her dog that lined up with various parts of her story. That was a great idea.</p> <p><a href="https://www.linkedin.com/in/kyle-davis-linux/?originalSubdomain=ca" target="_blank" rel="noreferrer">Kyle Davis</a> offered talks on <a href="https://github.com/valkey-io/valkey" target="_blank" rel="noreferrer">valkey</a> and <a href="https://github.com/bottlerocket-os/bottlerocket" target="_blank" rel="noreferrer">bottlerocket</a>. There was plenty about the redis and valkey story that I didn&rsquo;t know and the context was useful. It looks like you can simply drop valkey into most redis environments without much disruption.</p> <p><a href="https://www.linkedin.com/in/thomascameron/" target="_blank" rel="noreferrer">Thomas Cameron</a> talked about running OKD on Fedora CoreOS in his home lab. There were quite a few steps, but he did a great job of connecting the dots between what needed to be done and why.</p> <h2 id="around-the-exhibit-hall" class="relative group">Around the exhibit hall <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#around-the-exhibit-hall" aria-label="Anchor">#</a></span></h2><p>I helped staff the Fedora/CoreOS booth and we had plenty of questions. Most questions were around the M1 Macbook running <a href="https://asahilinux.org/" target="_blank" rel="noreferrer">Asahi Linux</a> that was on the table. ๐Ÿ˜‰</p> <p>There were still quite a few misconceptions around the CentOS Stream changes, as well as how AlmaLinux and Rocky Linux fit into the picture. Our booth was right next to the AlmaLinux booth and I had the opportunity to meet <a href="https://jonathanspw.com/about/" target="_blank" rel="noreferrer">Jonathan Wright</a>. That was awesome!</p> <p><strong>I can&rsquo;t wait for next year&rsquo;s event.</strong></p>Roll your own static blog analyticshttps://major.io/p/static-blog-analytics/Thu, 04 Apr 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/static-blog-analytics/<p>Static blogs come with tons of advantages. They&rsquo;re cheap to serve. You store all your changes in git. People with spotty internet connections can clone your blog and run it locally.</p> <p><strong>However, one of the challenges that I&rsquo;ve run into over the years is around analytics.</strong></p> <p>I could quickly add Google Analytics to the site and call it a day, but is that a good idea? Many browsers have ad blocking these days and the analytics wouldn&rsquo;t even run. For those that don&rsquo;t have an ad blocker, do I want to send more data about them to Google? ๐Ÿ™ƒ</p> <p>How about running my own self-hosted analytics platform? That&rsquo;s pretty easy with containers, but most ad blockers know about those, too.</p> <p>This post talks about how to host a static blog in a container behind a <a href="https://caddyserver.com/" target="_blank" rel="noreferrer">Caddy</a> web server. We will use <a href="https://goaccess.io/" target="_blank" rel="noreferrer">goaccess</a> to analyze the log files on the server itself to avoid dragging in an analytics platform.</p> <h2 id="why-do-you-need-analytics" class="relative group">Why do you need analytics? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#why-do-you-need-analytics" aria-label="Anchor">#</a></span></h2><p>Yes, yes, I know this comes from the guy who wrote a post about <a href="https://major.io/p/how-i-write-blog-posts/">writing for yourself</a>, but sometimes I like to know which posts are popular with other people. I also like to know if something&rsquo;s misconfigured and visitors are seeing 404 errors for pages which should be working.</p> <p>It can also be handy to know when someone else is <a href="https://major.io/p/puppy-linux-icanhazip-and-tin-foil-hats/">writing about you</a>, especially when those things are incorrect. ๐Ÿ˜‰</p> <p>So my goals here are these:</p> <ul> <li>Get some basic data on what&rsquo;s resonating with people and what isn&rsquo;t</li> <li>Find configuration errors that are leading visitors to error pages</li> <li>Learn more about who is linking to the site</li> <li>Do all this without impacting user privacy through heavy javascript trackers</li> </ul> <h2 id="what-are-the-ingredients" class="relative group">What are the ingredients? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-are-the-ingredients" aria-label="Anchor">#</a></span></h2><p>There are three main pieces:</p> <ol> <li>Caddy, a small web server that runs really well in containers</li> <li>This blog, which is written with <a href="https://gohugo.io/" target="_blank" rel="noreferrer">Hugo</a> and <a href="https://github.com/major/major.io" target="_blank" rel="noreferrer">stored in GitHub</a></li> <li>Goaccess, a log analyzer with a capability to do live updates via websockets</li> </ol> <p>Caddy will write logs to a location that goaccess can read. In turn, goaccess will write log analysis to an HTML file that caddy can serve. The HTML file served by caddy will open a websocket to goaccess for live analytics.</p> <h2 id="a-static-blog-in-a-container" class="relative group">A static blog in a container? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#a-static-blog-in-a-container" aria-label="Anchor">#</a></span></h2><p>We can pack a static blog into a very thin container with an extremely lightweight web server. After all, caddy can handle automatic TLS certificate installation, logging, and caching. That just means we need the most basic webserver in the container itself.</p> <p>I was considering a second caddy container with the blog content in it until I stumbled upon a great post by Florin Lipan about <a href="https://lipanski.com/posts/smallest-docker-image-static-website" target="_blank" rel="noreferrer">The smallest Docker image to serve static websites</a>. He went down a rabbit hole to make the smallest possible web server container with busybox.</p> <p>His first stop led to a 1.25MB container, and that&rsquo;s tiny enough for me.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> ๐Ÿค</p> <p>I built a <a href="https://github.com/major/major.io/blob/main/.github/workflows/container.yml" target="_blank" rel="noreferrer">container workflow</a> in GitHub Actions that builds a container, puts the blog in it, and <a href="https://github.com/major/major.io/pkgs/container/major.io" target="_blank" rel="noreferrer">stores that container as a package</a> in the GitHub repository. It all starts with a brief Dockerfile:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">docker.io/library/busybox:1.36.1</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> adduser -D static<span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">USER</span><span class="w"> </span><span class="s">static</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/home/static</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">COPY</span> ./public/ /home/static<span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;busybox&#34;</span><span class="p">,</span> <span class="s2">&#34;httpd&#34;</span><span class="p">,</span> <span class="s2">&#34;-f&#34;</span><span class="p">,</span> <span class="s2">&#34;-p&#34;</span><span class="p">,</span> <span class="s2">&#34;3000&#34;</span><span class="p">]</span><span class="err"> </span></span></span></code></pre></div><p>We start with busybox, add a user, put the website content into the user&rsquo;s home directory, and start busybox&rsquo;s <code>httpd</code> server. The container starts up and serves the static content on port 3000.</p> <h2 id="caddy-logs" class="relative group">Caddy logs <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#caddy-logs" aria-label="Anchor">#</a></span></h2><p>Caddy writes its logs in a JSON format and goaccess already knows how to parse caddy logs. Our first step is to get caddy writing some logs. In my case, I have a directory called <code>caddy/logs/</code> in my home directory where those logs are written.</p> <p>I&rsquo;ll mount the log storage into the caddy container and mount one extra directory to hold the HTML file that goaccess will write. Here&rsquo;s my <code>docker-compose.yaml</code> excerpt:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">caddy</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/major/caddy:main</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">caddy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;80:80/tcp&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;443:443/tcp&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;443:443/udp&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./caddy/Caddyfile:/etc/caddy/Caddyfile:Z</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">caddy_data:/data</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">caddy_config:/config</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># Caddy writes logs here ๐Ÿ‘‡</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./caddy/logs:/logs:z</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># This is for goaccess to write its HTML file ๐Ÿ‘‡</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./storage/goaccess_major_io:/var/www/goaccess_major_io:z</span><span class="w"> </span></span></span></code></pre></div><p>Now we need to update the <code>Caddyfile</code> to tell caddy where to place the logs and add a <code>reverse_proxy</code> configuration for our new container that serves the blog:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Caddyfile" data-lang="Caddyfile"><span class="line"><span class="cl"><span class="gh">major.io</span> <span class="p">{</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> # We will set up this container in a moment ๐Ÿ‘‡ </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">reverse_proxy</span> <span class="s">major_io:3000</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">lb_try_duration</span> <span class="mi">30s</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> # Tell Caddy to write logs to `/logs` which </span></span></span><span class="line"><span class="cl"><span class="c1"> # is `storage/logs` on the host: </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">log</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">output</span> <span class="s">file</span> <span class="s">/logs/major.io-access.log</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">roll_size</span> <span class="s">1024mb</span> </span></span><span class="line"><span class="cl"> <span class="k">roll_keep</span> <span class="mi">20</span> </span></span><span class="line"><span class="cl"> <span class="k">roll_keep_for</span> <span class="mi">720h</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><p>Great! We now have the configuration in place for caddy to write the logs and the caddy container can mount the log and analytics storage.</p> <h2 id="enabling-analytics" class="relative group">Enabling analytics <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#enabling-analytics" aria-label="Anchor">#</a></span></h2><p>We&rsquo;re heading back to the <code>docker-compose.yml</code> file once more, this time to set up a goaccess container:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">goaccess_major_io</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/allinurl/goaccess:latest</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">goaccess_major_io</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># Mount caddy&#39;s log files ๐Ÿ‘‡</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;./caddy/logs:/var/log/caddy:z&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># Mount the directory where goaccess writes the analytics HTML ๐Ÿ‘‡</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;./storage/goaccess_major_io:/var/www/goaccess:rw&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/var/log/caddy/major.io-access.log --log-format=CADDY -o /var/www/goaccess/index.html --real-time-html --ws-url=wss://stats.major.io:443/ws --port=7890 --anonymize-ip --ignore-crawlers --real-os&#34;</span><span class="w"> </span></span></span></code></pre></div><p>This gets us a goaccess container to parse the logs from caddy. We need to update the caddy configuration so that we can reach the goaccess websocket for live updates:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Caddyfile" data-lang="Caddyfile"><span class="line"><span class="cl"><span class="gh">stats.major.io</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">root</span> <span class="nd">*</span> <span class="s">/var/www/goaccess_major_io</span> </span></span><span class="line"><span class="cl"> <span class="k">file_server</span> </span></span><span class="line"><span class="cl"> <span class="k">reverse_proxy</span> <span class="nd">/ws</span> <span class="s">goaccess_major_io:7890</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>At this point, we have caddy writing logs in the right place, goaccess can read them, and the analytics output is written to a place where caddy can serve it. We&rsquo;ve also exposed the websocket from goaccess for live updates.</p> <h2 id="serving-the-blog" class="relative group">Serving the blog <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#serving-the-blog" aria-label="Anchor">#</a></span></h2><p>We&rsquo;ve reached the most important part!</p> <p>We added the caddy configuration to reach the blog container earlier, but now it&rsquo;s time to deploy the container itself. As a reminder, this is the container with busybox and the blog content that comes from GitHub Actions.</p> <p>The <code>docker-compose.yml</code> configuration here is <em>very basic</em>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">major_io</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/major/major.io:main</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">major_io</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w"> </span></span></span></code></pre></div><p>Caddy will connect to this container on port 3000 to serve the blog. (We set port 3000 in the original <code>Dockerfile</code>).</p> <p>At this point, everything should be set to go. Make it live with:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="go">docker-compose up -d </span></span></span></code></pre></div><p>This should bring up the goaccess and blog containers while also restarting caddy. The website should be visible now at <a href="https://major.io/" target="_blank" rel="noreferrer">major.io</a> (and that&rsquo;s how you&rsquo;re reading this today).</p> <h2 id="what-about-new-posts" class="relative group">What about new posts? <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-about-new-posts" aria-label="Anchor">#</a></span></h2><p>I&rsquo;m glad you asked! That was something I wondered about as well. <strong>How do we get the new blog content down to the container when a new post is written?</strong> ๐Ÿค”</p> <p>As I&rsquo;ve <a href="https://major.io/p/watchtower/">written in the past</a>, I like using <a href="https://containrrr.dev/watchtower/" target="_blank" rel="noreferrer">watchtower</a> to keep containers updated. Watchtower offers an HTTP API interface for webhooks to initiate container updates. We can trigger that update via a simple curl request from GitHub Actions when our container pipeline runs.</p> <p>My <a href="https://github.com/major/major.io/blob/main/.github/workflows/container.yml" target="_blank" rel="noreferrer">container workflow</a> has a brief bit at the end that does this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><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">Update the blog container</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.event_name != &#39;pull_request&#39;</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"> curl -s -H &#34;Authorization: Bearer ${WATCHTOWER_TOKEN}&#34; \ </span></span></span><span class="line"><span class="cl"><span class="sd"> https://watchtower.thetanerd.com/v1/update</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">WATCHTOWER_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.WATCHTOWER_TOKEN }}</span><span class="w"> </span></span></span></code></pre></div><p>You can enable this in watchtower with a few new environment variables in your <code>docker-compose.yml</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-YAML" data-lang="YAML"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">watchtower</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># New environment variables ๐Ÿ‘‡</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="l">WATCHTOWER_HTTP_API_UPDATE=true</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">WATCHTOWER_HTTP_API_TOKEN=SUPER-SECRET-TOKEN-PASSWORD</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">WATCHTOWER_HTTP_API_PERIODIC_POLLS=true</span><span class="w"> </span></span></span></code></pre></div><p><code>WATCHTOWER_HTTP_API_UPDATE</code> enables the updating via API and <code>WATCHTOWER_HTTP_API_TOKEN</code> sets the token required when making the API request. If you set <code>WATCHTOWER_HTTP_API_PERIODIC_POLLS</code> to <code>true</code>, watchtower will still periodically look for updates to containers even if an API request never appeared. By default, watchtower will stop doing periodic updates if you enable the API.</p> <p>This is working on my site right now and you can view my public blog stats on <a href="https://stats.major.io" target="_blank" rel="noreferrer">stats.major.io</a>. ๐ŸŽ‰</p> <div class="footnotes" role="doc-endnotes"> <hr> <ol> <li id="fn:1"> <p>Florin went all the way down to 154KB and I was extremely impressed. However, I&rsquo;m not too worried about an extra megabyte here. ๐Ÿ˜‰&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p> </li> </ol> </div>Connect Caddy to Porkbunhttps://major.io/p/caddy-porkbun/Thu, 29 Feb 2024 00:00:00 +0000major@mhtx.net (Major Hayden)https://major.io/p/caddy-porkbun/<p>I recently told a coworker about <a href="https://caddyserver.com/" target="_blank" rel="noreferrer">Caddy</a>, a small web and proxy server with a very simple configuration. It also has a handy feature where it manages your TLS certificate for you automatically.</p> <p>However, one problem I had at home with my <a href="https://fedoraproject.org/coreos/" target="_blank" rel="noreferrer">CoreOS</a> deployment is that I don&rsquo;t have inbound network access to handle the certificate verification process. Most automated certificate vendors need to reach your web server to verify that you have control over your domain.</p> <p>This post talks about how to work around this problem with domains registered at <a href="https://porkbun.com/" target="_blank" rel="noreferrer">Porkbun</a>.</p> <h2 id="dns-validation" class="relative group">DNS validation <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#dns-validation" aria-label="Anchor">#</a></span></h2><p>Certificate providers usually default to verifying domains by making a request to your server and retrieving a validation code. If your systems are all behind a firewall without inbound access from the internet, you can use DNS validation instead.</p> <p>The process looks something like this:</p> <ol> <li>You tell the certificate provider the domain names you want on your certificate</li> <li>The certificate provider gives you some DNS records to add wherever you host your DNS records</li> <li>You add the DNS records</li> <li>You get your certificates once the certificate provider verifies the records.</li> </ol> <p>You can do this manually with something like <a href="https://github.com/acmesh-official/acme.sh" target="_blank" rel="noreferrer">acme.sh</a> today, but it&rsquo;s <strong>painful</strong>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Make the initial certificate request</span> </span></span><span class="line"><span class="cl">acme.sh --issue --dns -d example.com <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --yes-I-know-dns-manual-mode-enough-go-ahead-please </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Add your DNS records manually.</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Verify the DNS records and issue the certificates.</span> </span></span><span class="line"><span class="cl">acme.sh --issue --dns -d example.com <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --yes-I-know-dns-manual-mode-enough-go-ahead-please --renew </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Copy the keys/certificates and configure your webserver.</span> </span></span></code></pre></div><p>We don&rsquo;t want to live this way.</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="581" height="355" class="mx-auto my-0 rounded-md" alt="do-not-want.gif" loading="lazy" decoding="async" src="https://major.io/p/caddy-porkbun/do-not-want.gif" /> </picture> </figure> </p> <p>Let&rsquo;s talk about how Caddy can help.</p> <h2 id="adding-porkbun-support-to-caddy" class="relative group">Adding Porkbun support to Caddy <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#adding-porkbun-support-to-caddy" aria-label="Anchor">#</a></span></h2><p>Caddy is a minimal webserver and <a href="https://github.com/caddy-dns/porkbun" target="_blank" rel="noreferrer">Porkbun support</a> doesn&rsquo;t get included by default. However, we can quickly add it via a simple container build:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">caddy:2.7.6-builder</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> xcaddy build <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --with github.com/caddy-dns/porkbun<span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">FROM</span><span class="w"> </span><span class="s">caddy:2.7.6</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">COPY</span> --from<span class="o">=</span>builder /usr/bin/caddy /usr/bin/caddy<span class="err"> </span></span></span></code></pre></div><p>This is a two stage container build where we compile the Porkbun support and then use that new <code>caddy</code> binary in the final container.</p> <p>We&rsquo;re not done yet!</p> <h2 id="automated-caddy-builds-with-updates" class="relative group">Automated Caddy builds with updates <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#automated-caddy-builds-with-updates" aria-label="Anchor">#</a></span></h2><p>I created a <a href="https://github.com/major/caddy" target="_blank" rel="noreferrer">GitHub repository</a> that builds the Caddy container for me and keeps it updated. There&rsquo;s a <a href="https://github.com/major/caddy/blob/main/.github/workflows/docker-publish.yml" target="_blank" rel="noreferrer">workflow to publish a container</a> to GitHub&rsquo;s container repository and I can pull containers from there on my various CoreOS machines.</p> <p>In addition, I use <a href="https://github.com/apps/renovate" target="_blank" rel="noreferrer">Renovate</a> to watch for Caddy updates. New updates come through a <a href="https://github.com/major/caddy/pull/10" target="_blank" rel="noreferrer">regular pull request</a> and I can apply them whenever I want.</p> <p> <figure> <picture class="mx-auto my-0 rounded-md" > <img width="918" height="461" class="mx-auto my-0 rounded-md" alt="Renovate pull request" loading="lazy" decoding="async" src="https://major.io/p/caddy-porkbun/pr_hu_86416cea2f9ab5b1.png" srcset="https://major.io/p/caddy-porkbun/pr_hu_3ca6ce3fc7071c7e.png 330w,https://major.io/p/caddy-porkbun/pr_hu_86416cea2f9ab5b1.png 660w ,https://major.io/p/caddy-porkbun/pr.png 918w ,https://major.io/p/caddy-porkbun/pr.png 918w " sizes="100vw" /> </picture> <figcaption class="text-center">Example pull request from Renovate</figcaption> </figure> </p> <h2 id="connecting-to-porkbun" class="relative group">Connecting to Porkbun <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#connecting-to-porkbun" aria-label="Anchor">#</a></span></h2><p>We start here by getting an API key to manage the domain at Porkbun.</p> <ol> <li>Log into your <a href="https://porkbun.com/account/domainsSpeedy" target="_blank" rel="noreferrer">Porkbun dashboard</a>.</li> <li>Click <strong>Details</strong> to the right of the domain you want to manage.</li> <li>Look for <strong>API Access</strong> in the leftmost column and turn it on.</li> <li>At the top right of the dashboard, click <strong>Account</strong> and then <strong>API Access</strong>.</li> <li>Add a title for your new API key, such as <em>Caddy</em>, and click <strong>Create API Key</strong>.</li> <li>Save the API key and secrey key that are displayed.</li> </ol> <p>Open up your Caddy configuration file (the <em>Caddyfile</em>) and add some configuration:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddyfile" data-lang="caddyfile"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">email</span> <span class="s">me@example.com</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> # Uncomment this next line if you want to get </span></span></span><span class="line"><span class="cl"><span class="c1"> # some test certificates first. </span></span></span><span class="line"><span class="cl"><span class="c1"> # acme_ca https://acme-staging-v02.api.letsencrypt.org/directory </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="k">acme_dns</span> <span class="s">porkbun</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">api_key</span> <span class="s">pk1_******</span> </span></span><span class="line"><span class="cl"> <span class="k">api_secret_key</span> <span class="s">sk1_******</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></span><span class="line"><span class="cl"><span class="gh">example.com</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">handle</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">respond</span> <span class="s2">&#34;Hello world!&#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></code></pre></div><p>Save the Caddyfile and restart your Caddy server or container. Caddy will immediately begin requesting your TLS certificates and managing your DNS records for those certificates. This normally finishes in less than 30 seconds or so during the first run.</p> <p>If you don&rsquo;t see the HTTPS endpoint working within a minute or two, be sure to check the Caddy logs. You might have a typo in a Porkbun API key or the domain you&rsquo;re trying to modify doesn&rsquo;t have the <strong>API Access</strong> switch enabled.</p> <div class="flex rounded-md bg-primary-100 px-4 py-3 dark:bg-primary-900"> <span class="pe-3 text-primary-400"> <span class="icon relative inline-block px-1 align-text-bottom"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 128c17.67 0 32 14.33 32 32c0 17.67-14.33 32-32 32S224 177.7 224 160C224 142.3 238.3 128 256 128zM296 384h-80C202.8 384 192 373.3 192 360s10.75-24 24-24h16v-64H224c-13.25 0-24-10.75-24-24S210.8 224 224 224h32c13.25 0 24 10.75 24 24v88h16c13.25 0 24 10.75 24 24S309.3 384 296 384z"/></svg> </span> </span> <span class="dark:text-neutral-300">Remember that Porkbun requires you to enable API access for each domain. API access is disabled at Porkbun by default.</span> </div> <p><strong>That&rsquo;s it!</strong> ๐ŸŽ‰</p> <h2 id="renewals" class="relative group">Renewals <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#renewals" aria-label="Anchor">#</a></span></h2><p>Caddy will keep watch over the certificates and begin the renewal process as the expiration approaches. It has a very careful retry mechanism that ensures your certificates are updated without tripping any rate limits at the certificate provider.</p> <h2 id="further-reading" class="relative group">Further reading <span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"><a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#further-reading" aria-label="Anchor">#</a></span></h2><p>Caddy&rsquo;s detailed documentation about <a href="https://caddyserver.com/docs/automatic-https" target="_blank" rel="noreferrer">Automatic HTTPS</a> and the <a href="https://caddyserver.com/docs/caddyfile/directives/tls" target="_blank" rel="noreferrer">tls configuration directive</a> should answer most questions about how the process works.</p>