<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blogging on ln --help</title>
    <link>https://blog.mei-home.net/tags/blogging/</link>
    <description>Recent content in Blogging on ln --help</description>
    <generator>Hugo -- 0.152.2</generator>
    <language>en</language>
    <lastBuildDate>Mon, 24 Aug 2026 23:50:15 +0200</lastBuildDate>
    <atom:link href="https://blog.mei-home.net/tags/blogging/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Sammelsurium II</title>
      <link>https://blog.mei-home.net/posts/sammelsurium-2/</link>
      <pubDate>Mon, 24 Aug 2026 23:50:15 +0200</pubDate>
      <guid>https://blog.mei-home.net/posts/sammelsurium-2/</guid>
      <description>A random mix of topics too small for their own posts but not ephemeral enough for a Fediverse post</description>
      <content:encoded><![CDATA[<p>We haven&rsquo;t had one of these in over a year, and there&rsquo;s a lot in my blogging
orgmode file that even my awe-inspiring powers of making a lot of words out of a
four word error message couldn&rsquo;t turn into a full blog post.</p>
<p>The idea of this series of posts is to gather all the things which are a bit
too long for just a Fedi post, but also too short for their own blog post.</p>
<p>Today, I finished another milestone for my <a href="https://blog.mei-home.net/tags/smoking/">Smokeweb</a>
project, namely the HTTP API. The next step will be changing the C++ client over
from using a local SQLite DB to the web API I&rsquo;ve been implementing over the last
couple of months. But before I do get to that, I found this a good spot to switch
gears a little bit and do some Homelabbing. There&rsquo;s a number of tasks I left
sitting in my task list in favor of not slowing down the Smokeweb project, and
I want to tackle at least a few of those now.</p>
<p>That is also the reason for this post, in a sense. Switching back to Homelabbing
from programming likely also means some more blog posts. Just because I enjoy
writing Homelab posts a lot more than programming posts. And I wanted to clean
the slate a bit and clean out my blogging org file.</p>
<h2 id="zoomable-figures-in-the-blog">Zoomable figures in the blog</h2>
<p>Something which vexed me for a while was the fact that figures in my blog posts,
especially snapshots of Grafana charts, were often times quite small and difficult
to decipher. After a little bit of searching, I found <a href="https://haseebmajid.dev/posts/2022-11-21-til-how-you-can-zoom-in-on-photos-in-your-hugo-posts/">this blog post</a>.</p>
<p>Following its instructions, I added the following code into <code>layouts/partials/extend_footer.html</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>{{ $mediumZoom := resources.Get &#34;js/medium-zoom.js&#34; | minify | fingerprint &#34;sha512&#34; }}
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ $mediumZoom.RelPermalink }}&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">integrity</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ $mediumZoom.Data.Integrity }}&#34;</span>
</span></span><span style="display:flex;"><span>&gt;&lt;/<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">images</span> <span style="color:#f92672">=</span> Array.<span style="color:#a6e22e">from</span>(document.<span style="color:#a6e22e">querySelectorAll</span>(<span style="color:#e6db74">&#34;.post-content img&#34;</span>));
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">images</span>.<span style="color:#a6e22e">forEach</span>((<span style="color:#a6e22e">img</span>) =&gt; {
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">mediumZoom</span>(<span style="color:#a6e22e">img</span>, {
</span></span><span style="display:flex;"><span>         <span style="color:#a6e22e">margin</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span> <span style="color:#75715e">/* The space outside the zoomed image */</span>,
</span></span><span style="display:flex;"><span>         <span style="color:#a6e22e">scrollOffset</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">40</span> <span style="color:#75715e">/* The number of pixels to scroll to close the zoom */</span>,
</span></span><span style="display:flex;"><span>         <span style="color:#a6e22e">container</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span> <span style="color:#75715e">/* The viewport to render the zoom in */</span>,
</span></span><span style="display:flex;"><span>         <span style="color:#a6e22e">template</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span> <span style="color:#75715e">/* The template element to display on zoom */</span>,
</span></span><span style="display:flex;"><span>         <span style="color:#a6e22e">background</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;rgba(0, 0, 0, 0.8)&#34;</span>,
</span></span><span style="display:flex;"><span>     });
</span></span><span style="display:flex;"><span> });
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><p>The blog post describes how to fetch the JS code from a CDN, but I wanted to
keep my blog self-contained, so I downloaded the script and put it into a file
at <code>assets/js/medium-zoom.js</code>. It is then loaded by this code from the above
snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>{{ $mediumZoom := resources.Get &#34;js/medium-zoom.js&#34; | minify | fingerprint &#34;sha512&#34; }}
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ $mediumZoom.RelPermalink }}&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">integrity</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ $mediumZoom.Data.Integrity }}&#34;</span>
</span></span><span style="display:flex;"><span>&gt;&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><p>The JS lib used can be found <a href="https://github.com/francoischalifour/medium-zoom">here</a>.</p>
<p><a href="https://digitaldrummerj.me/hugo-asset-pipeline/">This blog post</a> is also worth
mentioning for explaining the Hugo asset pipeline and how to use it to minify the
JS.</p>
<p>And with that, all images on the blog can be clicked, which opens them up in their
original size.</p>
<h2 id="the-grafana-image-renderer">The Grafana image renderer</h2>
<p>Related to the point above, I learned about the Grafana image renderer. It is a
Grafana plugin which allows making screenshots of panels, without making actual
screenshots. The plugin can be found <a href="https://grafana.com/docs/grafana/latest/setup-grafana/image-rendering/">here</a>.</p>
<p>Looking at the plugin, the first thing which becomes clear is that for some reason,
it is just Chromium in a trench coat. Yupp, the entire browser, delivered in a
container image. &#x1f937;</p>
<p>I&rsquo;m using the <a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack">kube-prometheus-stack</a>
Helm chart for my monitoring stack, which in turn contains the Grafana Helm chart.
With that, I was able to relatively simply enable the image renderer by adding
these options to my <code>values.yaml</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">grafana</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">imageRenderer</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">serviceMonitor</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">2Gi</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">500m</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">2Gi</span>
</span></span></code></pre></div><p>This worked, in the sense that I was getting an additional option in the <code>...</code>
menu to take a screenshot of the chart. But there were a couple of issues:</p>
<ul>
<li>It only works for actual panels in dashboards, not e.g. in Explore mode</li>
<li>Only makes a screenshot of the full panel, so cannot e.g. filter out a couple of the plots in it</li>
<li>Does not show the legend, whether it&rsquo;s switched on or not</li>
</ul>
<p>Due to these downsides, I ended up removing it again and will continue to take
screenshots with the GNU image manipulation program.</p>
<h2 id="disabling-rook-ceph-disk-discovery-on-certain-nodes">Disabling Rook Ceph disk discovery on certain nodes</h2>
<p>Let&rsquo;s see whether I can figure out what this one is about, because the entire
section on it in my notes is exactly what you see in the heading. &#x1f601;</p>
<p>I seem to dimly remember that Rook Ceph regularly runs disk discovery on all
nodes via its orchestrator, likely because it has the ability to automatically
induct new disks into the storage cluster. I&rsquo;ve got that functionality disabled,
but Ceph might still want to know what&rsquo;s available. I generally don&rsquo;t mind that,
it only takes about a second or so. But what does annoy me is my external backup
disk. That disk automatically spins down after the nightly backup, and it&rsquo;s
connected via USB to one of the hosts in my k8s cluster. So whenever Ceph runs
its discovery, the disk spins up.</p>
<p>Aha, found the commit! The discovery is actually run via a one-shot Pod, and that
Pod&rsquo;s node affinity can be configured. I restricted the discovery runs to the
actual Ceph nodes in my cluster by adding these lines to the Rook operator chart&rsquo;s
<code>values.yaml</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">discover</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeAffinity</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requiredDuringSchedulingIgnoredDuringExecution</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">nodeSelectorTerms</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/role&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#e6db74">&#34;ceph&#34;</span>
</span></span></code></pre></div><p>Note: This is the Rook operator&rsquo;s chart, no the cluster chart.</p>
<p>And with that, the discovery no longer runs on the non-Ceph nodes. This also
had the secondary advantage that this also prevents all the disks from the non-Ceph
nodes showing up in the Ceph web GUI as potential Ceph disks.</p>
<h2 id="using-json-to-check-if-ceph-osds-are-up-again-after-a-reboot">Using JSON to check if Ceph OSDs are up again after a reboot</h2>
<p>A while back, I <a href="https://blog.mei-home.net/posts/k8s-migration-7-node-updates/">wrote about my host update Ansible playbook</a>.
One part was this task:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wait for OSDs to start</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">cnc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">my_user</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">ceph</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">command</span>: <span style="color:#ae81ff">kceph --operator-namespace rook-ceph -n rook-cluster ceph osd status &#34;{{ ansible_hostname }}&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">register</span>: <span style="color:#ae81ff">ceph_end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">until</span>: <span style="color:#e6db74">&#39;(ceph_end.stdout | regex_findall(&#34;.*,up.*&#34;, multiline=True) | list | length) == (ceph_end.stdout_lines | length - 1)&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">retries</span>: <span style="color:#ae81ff">12</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">10</span>
</span></span></code></pre></div><p>It waits for some time for the OSD Pods to come up again. The intention is to
make sure that one Ceph host is fully up again before the next one is taken down
for the update and restart. In the above task, I&rsquo;m using a regex on the ceph
command&rsquo;s stdout output.</p>
<p>I didn&rsquo;t like that, as the output really wasn&rsquo;t very parseable, and I had to make
a number of changes over time to accommodate changes in the output format. But
the <code>ceph osd status</code> command also has a JSON output, which I deemed to be more
stable and easier to parse, so now the command looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wait for OSDs to start</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">cnc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">my_user</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">ceph</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">command</span>: <span style="color:#ae81ff">kceph --operator-namespace rook-ceph -n rook-cluster ceph osd status &#34;{{ ansible_hostname }}&#34; --format json</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">register</span>: <span style="color:#ae81ff">ceph_end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">until</span>: <span style="color:#e6db74">&#34;(ceph_end.stdout | trim | from_json | community.general.json_query(&#39;OSDs[*].state&#39;) | select(&#39;contains&#39;, &#39;up&#39;) | length) == (ceph_end.stdout | trim | from_json | community.general.json_query(&#39;OSDs[*]&#39;) | length)&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">retries</span>: <span style="color:#ae81ff">12</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">10</span>
</span></span></code></pre></div><p>No actual functional change, but better stability.</p>
<h2 id="forgejo-outage-due-to-full-disk">Forgejo outage due to full disk</h2>
<p>A couple of weeks ago, my Forgejo instance suddenly stopped accepting my Git
pushes. The web interface itself still looked fine. Looking at the logs, I found
a couple of no space left on device errors. That surprised me, as I hadn&rsquo;t recently
added any new repos or big commits. But the 15GB disk was still full. Logging into
the container, I found the culprit: Lots of files called something like <code>/objects/pack/tmp_pack_VjJYG2</code>
in the git directories of some of my larger repos.</p>
<p>Those larger repos are mostly collections of Ebola reporting files from the
2014 outbreak in West Africa and the repos of some Sins of a Solar Empire mods.</p>
<p>Looking at the disk usage for the Forgejo data volume, I saw that the issue wasn&rsquo;t
a persistent raise, but rather an increase in steps:</p>
<figure>
    <img loading="lazy" src="forgejo-vol.png"
         alt="A screenshot of a Grafana time series panel. It shows the data utilization of my Forgejo volume. On the left is the size, in bytes. It starts out around 2026-07-27 at a bit under 10 GB. Then comes the first jump straight up at the 28th to about 12 TB. Then the utilization is flat again. The pattern repeats on 31st with a jump to 15 GB, then on the 3rd of August to 15.5GB, on the 6th to 19 GB, on the 11th to 22 GB and then the final jump to 25 GB on the 14th. Finally, on the 16th, it goes straight down to below 10 GB."/> <figcaption>
            <p>The storage usage for my Forgejo volume. The y-axis shows GB.</p>
        </figcaption>
</figure>

<p>After having a look at the logs for one of the days with an increase, I found log
lines like this:</p>
<pre tabindex="0"><code>Repository garbage collection failed for ... Error: context deadline exceeded
</code></pre><p>This indicated that the garbage collection was running into a timeout, which then
likely leaves behind orphan files. What I haven&rsquo;t been able to figure out is why
this suddenly started to be a problem. As I noted above, I have neither added nor
removed repos recently.</p>
<p>The fix was then relatively simple. I added the following into the <code>values.yaml</code>
file for my Forgejo deployment to increase the timeout of the repo garbage
collection task:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">gitea</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">config</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cron.git_gc_repos</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ENABLED</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">RUN_AT_START</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">SCHEDULE</span>: <span style="color:#e6db74">&#34;@every 72h&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">TIMEOUT</span>: <span style="color:#e6db74">&#34;15m&#34;</span>
</span></span></code></pre></div><p>This fixed the issue, and I haven&rsquo;t seen any increases in space consumption since
then.</p>
<h2 id="it-was-dns">It was DNS!</h2>
<p>Because, of course it was. Back on 2026-07-20, I woke up in the morning, got my
morning coffee and opened Mastodon on my tablet. Just to get a bad gateway error
reply from Firefox. I put it down to some sort of issue with my Mastodon instance
and headed into work, as my typical workday mornings don&rsquo;t have any time blocked
for Homelab debugging.</p>
<p>Scandalous, I know.</p>
<p>At work, while running a few compiles of our full product against an empty cache
to &ldquo;verify it still builds without access to any caches&rdquo;, I poked at my Homelab
a bit more. And I was actually getting pings back from all of my hosts. So at
least I could be sure it wasn&rsquo;t anything too catastrophic.</p>
<p>To be precise, externally visible services showed the bad gateway error, while
internal services just showed server not found. Running a couple of <code>dig</code> commands,
I found that indeed, everything internal was just throwing NXDOMAIN back at me.
Then I explicitly send the queries directly to the Pi4 hosting my PowerDNS
instance, which serves as the internal authoritative server for mei-home.net.
And it returned the expected and correct responses.</p>
<p>It was just the Unbound instance on my OPNsense box which returns NXDOMAIN,
even though it was supposed to forward to aforementioned PowerDNS instance. And
because that is the DNS server my DHCP hands out, I was not getting access to
anything. But everything in the k8s cluster was still perfectly fine, because
everything there uses the cluster-internal DNS. I wasn&rsquo;t able to figure out
what happened, but after restarting Unbound, it started forwarding queries for
mei-home.net domains to the PowerDNS again, and everything immediately returned
to normal.</p>
<p>No idea what that was. Perhaps unbound just didn&rsquo;t feel like getting out of bed
that morning? Had to skip its morning coffee and thus wasn&rsquo;t at its best game
before I slapped it smartly around the head? &#x1f937;</p>
<h2 id="using-miller-for-command-line-spreadsheeting">Using miller for command line spreadsheeting</h2>
<p>Last but not least, a small tool I&rsquo;ve found useful: <a href="https://github.com/johnkerl/miller">miller</a>.
It&rsquo;s a command line tool for working with stuff like CSV files. Basically a bit
of Excel for the command line. I was interested in it due to the table I&rsquo;ve been
keeping at the end of my <a href="https://blog.mei-home.net/tags/tr%C3%A4velling/">trävelling</a>
posts. Up to now, I&rsquo;ve always created that table by copying the one from the last
entry in the series and adding the values of the new journey. But having a bit of
time on my hands during another train journey, I decided to automate the table
creation process a little bit, which is when I came across miller.</p>
<p>First, the input file wit the data for my train journeys looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csv" data-lang="csv"><span style="display:flex;"><span><span style="color:#e6db74">Date</span>,<span style="color:#e6db74">Inconvenience</span>,<span style="color:#e6db74">Start</span>,<span style="color:#e6db74">Planned Arrival</span>,<span style="color:#e6db74">Actual Arrival</span>,<span style="color:#e6db74">Intermediate Delays</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">2026-02-12</span>,<span style="color:#e6db74">6</span>,<span style="color:#e6db74">2026-02-12T09:00</span>,<span style="color:#e6db74">2026-02-12T15:13</span>,<span style="color:#e6db74">2026-02-12T15:13</span>,<span style="color:#e6db74">5m</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">2026-02-15</span>,<span style="color:#e6db74">0</span>,<span style="color:#e6db74">2026-02-15T09:00</span>,<span style="color:#e6db74">2026-02-15T15:11</span>,<span style="color:#e6db74">2026-02-15T16:09</span>,<span style="color:#e6db74">0m</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">2026-04-23</span>,<span style="color:#e6db74">2</span>,<span style="color:#e6db74">2026-04-23T09:00</span>,<span style="color:#e6db74">2026-04-23T15:17</span>,<span style="color:#e6db74">2026-04-23T16:00</span>,<span style="color:#e6db74">0m</span>
</span></span></code></pre></div><p>From that, I&rsquo;d like to create a table like this for the blog post:</p>
<table>
  <thead>
      <tr>
          <th>Date</th>
          <th style="text-align: right">Inconvenience</th>
          <th style="text-align: right">Dest Lateness</th>
          <th style="text-align: right">Overall Lateness</th>
          <th style="text-align: right">Duration</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2026-02-12</td>
          <td style="text-align: right">6</td>
          <td style="text-align: right">0m</td>
          <td style="text-align: right">5m</td>
          <td style="text-align: right">6h13m</td>
      </tr>
      <tr>
          <td>2026-02-15</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">58m</td>
          <td style="text-align: right">58m</td>
          <td style="text-align: right">7h9m</td>
      </tr>
      <tr>
          <td>2026-04-23</td>
          <td style="text-align: right">2</td>
          <td style="text-align: right">43m</td>
          <td style="text-align: right">43m</td>
          <td style="text-align: right">7h00m</td>
      </tr>
  </tbody>
</table>
<p>My goal was to get something which computed all of the values from the input data
and output it in Markdown table format so I just needed to copy+paste it.</p>
<p>To achieve that goal, I implemented the following miller file:</p>
<pre tabindex="0"><code>begin {
@inconvSum = 0;
@DestLatenessSum = 0;
@OverallLatenessSum = 0;
@DurationSum = 0;
@Temp1 = 0;
}
  $DurationSec = int(strptime(${Actual Arrival}, &#34;%FT%H:%M&#34;) - strptime(${Start}, &#34;%FT%H:%M&#34;));
  $Duration = sec2dhms($DurationSec);
  $DestLatenessSec = int(strptime(${Actual Arrival}, &#34;%FT%H:%M&#34;) - strptime(${Planned Arrival}, &#34;%FT%H:%M&#34;));
  ${Dest Lateness} = sec2dhms($DestLatenessSec);
  $OverallLatenessSec = dhms2sec(${Intermediate Delays}) + dhms2sec(${Dest Lateness});
  ${Overall Lateness} = sec2dhms($OverallLatenessSec);
  @inconvSum += $Inconvenience;
  @DestLatenessSum += $DestLatenessSec;
  @OverallLatenessSum += $OverallLatenessSec;
  @DurationSum += $DurationSec;
  @Temp1 += $DestLatenessSec;
  $* = {
     &#34;Date&#34;: $Date,
     &#34;Inconvenience&#34;: $Inconvenience,
     &#34;Dest Lateness&#34;: ${Dest Lateness},
     &#34;Overall Lateness&#34;: ${Overall Lateness},
     &#34;Duration&#34;: $Duration
  };
end {
@inconvAvg = @inconvSum / NR;
@DestLatenessAvg = sec2dhms(int(@DestLatenessSum / NR));
@DestLatenessOut = @DestLatenessAvg . &#34;/&#34; . sec2dhms(@DestLatenessSum);
@OverallLatenessAvg = sec2dhms(int(@OverallLatenessSum / NR));
@OverallLatenessOut = @OverallLatenessAvg . &#34;/&#34; . sec2dhms(@OverallLatenessSum);
@DurationAvg = sec2dhms(int(@DurationSum / NR));
@DurationOut = @DurationAvg . &#34;/&#34; . sec2dhms(@DurationSum);
emitf @Totals,
      @inconvAvg,
      @DestLatenessOut,
      @OverallLatenessOut,
      @DurationOut;
}
</code></pre><p>It took some delightful fiddling to get this done, but now it spits out the entire
table so I can just copy+paste it into place.</p>
<p>Let&rsquo;s just say I will have to write a lot of those Trävelling with Deutsche Bahn
posts to make the time it cost me worth it. &#x1f605;</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
