<?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>TIL on foosel.net</title><link>https://foosel.net/til/</link><description>Recent content in TIL on foosel.net</description><generator>Hugo</generator><language>en-us</language><copyright>Gina Häußge (foosel)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://foosel.net/til/feed.xml" rel="self" type="application/rss+xml"/><item><title>TIL: How to make a necklace for holding my glasses</title><link>https://foosel.net/til/2026-06-24-how-to-make-a-necklace-for-holding-my-glasses/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2026-06-24-how-to-make-a-necklace-for-holding-my-glasses/</guid><description>&lt;p&gt;Thanks to the current heat wave and thus a shear TON of sun, I&amp;rsquo;m currently running around with both my prescription sunglasses and my regular glasses whenever I&amp;rsquo;m out and about&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, as while the sunglasses are great for outdoors, they are quite dark and thus a bit tricky indoors. Carrying around two pairs of glasses gets annoying quickly as I have to put the one I&amp;rsquo;m not wearing somewhere, I don&amp;rsquo;t want to carry it in my hand, I don&amp;rsquo;t want to lug around a case for it to put in a pocket or bag, and as it turns out just hooking it on my shirt works somewhat, but doesn&amp;rsquo;t feel safe.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Thanks to the current heat wave and thus a shear TON of sun, I&rsquo;m currently running around with both my prescription sunglasses and my regular glasses whenever I&rsquo;m out and about<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, as while the sunglasses are great for outdoors, they are quite dark and thus a bit tricky indoors. Carrying around two pairs of glasses gets annoying quickly as I have to put the one I&rsquo;m not wearing somewhere, I don&rsquo;t want to carry it in my hand, I don&rsquo;t want to lug around a case for it to put in a pocket or bag, and as it turns out just hooking it on my shirt works somewhat, but doesn&rsquo;t feel safe.</p>
<p>Enough is enough&hellip; enter a glasses holder in the shape of an adjustable necklace that I just created from paracord.</p>
<p><img src="https://foosel.net/til/2026-06-24-how-to-make-a-necklace-for-holding-my-glasses/glasses-holder-1.jpg" alt="The finished holder: a paracord loop of adjustable size, with an attached key ring" loading="lazy">
</p>
<p>I took a <strong>small key ring (18mm dia)</strong> I happened to have on hand, and some <strong>1.5mm mini paracord</strong> I also happened to have on hand<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.
I measured out the needed length of paracord by hanging it around my neck so that both ends met just below my chest. Then I put two <a href="https://www.animatedknots.com/figure-8-knot">figure 8 knots</a> into the ends and around itself, so that I got an adjustable closure.</p>
<p><img src="https://foosel.net/til/2026-06-24-how-to-make-a-necklace-for-holding-my-glasses/knot.jpg" alt="Close-up of how I tied the figure 8 knot" loading="lazy">
</p>
<p>Finally, I put the key ring on in the middle of the necklace using a basic <a href="https://www.animatedknots.com/cow-hitch-knot-end-method">cow hitch</a>.</p>
<p>Aaaand done!</p>
<p><img src="https://foosel.net/til/2026-06-24-how-to-make-a-necklace-for-holding-my-glasses/glasses-holder-2.jpg" alt="The holder around my neck, my regular glasses hooked onto it" loading="lazy">
</p>
<p>Holds my regular glasses or my sunglasses just fine, can be adjusted in length to easily get over my head but also not wiggle around too much. Somewhat fashionable too, but most importantly functional. I might make another one out of leather, but for now my problem is solved.</p>
<p><em><strong>Update from 2026-06-26</strong>: I&rsquo;ve made another one using a black leather cord and a metal o-ring and now it almost looks like a fashion accessory vs a practical solution to a problem. It turns out that this is also a nice fidget toy.</em></p>
<p><img src="https://foosel.net/til/2026-06-24-how-to-make-a-necklace-for-holding-my-glasses/glasses-holder-3.jpg" alt="The second holder around my neck: a black leather cord and a dark metal o-ring." loading="lazy">
</p>
<p><em>I&rsquo;m quite happy with that!</em></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Which given temperatures of 35°C+ I currently avoid like the plague, but errands don&rsquo;t run themselves.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Having a ton of hobbies and interests can come in handy sometimes&hellip;&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>TIL: How to manage this Hugo based page from my Android phone in 2026</title><link>https://foosel.net/til/2026-06-19-how-to-manage-this-page-from-my-phone-in-2026/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2026-06-19-how-to-manage-this-page-from-my-phone-in-2026/</guid><description>&lt;p&gt;This TIL post is basically an update to my three year old post &lt;a href="https://foosel.net/blog/2023-01-21-hugo-meet-android/"&gt;&amp;ldquo;Hugo, meet Android&amp;rdquo;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I recently got a new phone and thus had to re-do the setup of my mobile blogging pipeline. So, basically, allow me to checkout, modify, test, commit and push the sources of this &lt;a href="https://gohugo.io/"&gt;Hugo based&lt;/a&gt; static page under Android. While doing so I noticed some necessary changes and improvements, and also wanted to make use of my &lt;a href="https://github.com/foosel/foosel.github.io/blob/master/Taskfile.yml"&gt;Taskfile&lt;/a&gt; which does most of the heavy lifting now, so here are my updated steps.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This TIL post is basically an update to my three year old post <a href="https://foosel.net/blog/2023-01-21-hugo-meet-android/">&ldquo;Hugo, meet Android&rdquo;</a>.</p>
<p>I recently got a new phone and thus had to re-do the setup of my mobile blogging pipeline. So, basically, allow me to checkout, modify, test, commit and push the sources of this <a href="https://gohugo.io/">Hugo based</a> static page under Android. While doing so I noticed some necessary changes and improvements, and also wanted to make use of my <a href="https://github.com/foosel/foosel.github.io/blob/master/Taskfile.yml">Taskfile</a> which does most of the heavy lifting now, so here are my updated steps.</p>
<h2 id="apps--permissions">Apps &amp; permissions</h2>
<ol>
<li>Install Tasker</li>
<li>From <strong>F-Droid</strong> install
<ul>
<li>Termux</li>
<li>Termux:Tasker</li>
<li>Markor</li>
</ul>
</li>
<li>Grant permissions
<ul>
<li>Tasker: Run commands via Termux</li>
<li>Termux: Draw over apps</li>
</ul>
</li>
</ol>
<h2 id="termux-setup">Termux setup</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># setup access to storage</span>
</span></span><span style="display:flex;"><span>termux-setup-storage
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># install packages</span>
</span></span><span style="display:flex;"><span>pkg upgrade
</span></span><span style="display:flex;"><span>pkg install git gh hugo iconv vim
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># log into github</span>
</span></span><span style="display:flex;"><span>gh auth login
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># navigate to Markor&#39;s document folder &amp; checkout sources</span>
</span></span><span style="display:flex;"><span>cd storage/shared/Documents/markor
</span></span><span style="display:flex;"><span>git clone --recurse-submodules https://github.com/foosel/foosel.github.io
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># create symlink in home</span>
</span></span><span style="display:flex;"><span>ln -s foosel.github.io ~/foosel.net
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># mark checkout as safe</span>
</span></span><span style="display:flex;"><span>git config --global --add safe.directory /storage/emulated/0/Documents/markor/foosel.github.io
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set git user metadata</span>
</span></span><span style="display:flex;"><span>git config --global user.email <span style="color:#e6db74">&#34;you@example.com&#34;</span>
</span></span><span style="display:flex;"><span>git config --global user.name <span style="color:#e6db74">&#34;Your Name&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># we want to use our Taskfile...</span>
</span></span><span style="display:flex;"><span>pkg install golang
</span></span><span style="display:flex;"><span>go install github.com/go-task/task/v3/cmd/task@latest
</span></span></code></pre></div><p>As Tasker will stay the <em>only</em> app on my system allowed to send commands to Termux, and I write all my tasks myself, I also added <code>allow-external-apps=true</code>  to <code>.termux/termux.properties</code> as described <a href="https://github.com/termux/termux-tasker#allow-external-apps-property-optional">here</a>. That allows me to directly execute stuff from Tasker by absolute path vs having to add a bunch of wrapper scripts to <code>~/.termux/shortcuts</code></p>
<p>To quickly test if everything works so far:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd ~/foosel.net
</span></span><span style="display:flex;"><span>task serve
</span></span></code></pre></div><p>This should build the page and serve it on <code>http://127.0.0.1:1313</code> (with auto reload).</p>
<h2 id="tasker-setup">Tasker setup</h2>
<p>I created a bunch of tasks, gave all of them an icon and added them to my launcher.</p>
<h3 id="serve-fooselnet">Serve foosel.net</h3>
<pre tabindex="0"><code>    Task: Serve foosel.net
    
    A1: Termux [
         Configuration: ~/go/bin/task serve
         
         Working Directory ✓
         Terminal Session ✓
         Wait For Result ✕
         Timeout (Seconds): 10
         Structure Output (JSON, etc): On ]
    
</code></pre><h3 id="pull-fooselnet">Pull foosel.net</h3>
<pre tabindex="0"><code>    Task: Pull foosel.net
    
    A1: Termux [
         Configuration: /data/data/com.termux/files/usr/bin/git pull
         
         Working Directory ✓
         Stdin ✕
         Custom Log Level null
         Terminal Session ✕
         Wait 
         Timeout (Seconds): 10
         Structure Output (JSON, etc): On ]
    
    A2: Flash [
         Text: Pulled foosel.net sources
         Continue Task Immediately: On
         Dismiss On Click: On ]
    
</code></pre><h3 id="push-fooselnet">Push foosel.net</h3>
<pre tabindex="0"><code>    Task: Push foosel.net
    
    A1: Termux [
         Configuration: /data/data/com.termux/files/usr/bin/git push
         
         Working Directory ✓
         Stdin ✕
         Custom Log Level null
         Terminal Session ✕
         Wait 
         Timeout (Seconds): 10
         Structure Output (JSON, etc): On ]
    
    A2: Flash [
         Text: Pushed foosel.net sources
         Continue Task Immediately: On
         Dismiss On Click: On ]
    
</code></pre><h3 id="new-blog-post">New Blog Post</h3>
<pre tabindex="0"><code>    Task: New Blog Post
    
    A1: Input Dialog [
         Title: New post
         Text: Enter title
         Close After (Seconds): 120
         Input Type: 540673 ]
    
    A2: Termux [
         Configuration: ~/.local/bin/task new-blog -- &#34;%input&#34;
         
         Working Directory ✓
         Stdin ✕
         Custom Log Level null
         Terminal Session ✕
         Wait For Re
         Timeout (Seconds): 10
         Structure Output (JSON, etc): On ]
    
    A3: JavaScriptlet [
         Code: var uri = &#34;content://net.dinglisch.android.taskerm.fileprovider/storage/emulated/0/Documents/markor/foosel.github.io/&#34; + stdout.trim();
         Auto Exit: On
         Timeout (Seconds): 45 ]
    
    A4: Send Intent [
         Cat: None
         Mime Type: text/markdown
         Data: %uri
         Package: net.gsantner.markor
         Class: net.gsantner.markor.activity.DocumentActivity
         Target: Activity ]
</code></pre><h3 id="new-til-post">New TIL Post</h3>
<pre tabindex="0"><code>    Task: New TIL Post
    
    A1: Input Dialog [
         Title: New TIL
         Text: Enter title
         Close After (Seconds): 120
         Input Type: 540673 ]
    
    A2: Termux [
         Configuration: ~/.local/bin/task new-til -- &#34;%input&#34;
         
         Working Directory ✓
         Stdin ✕
         Custom Log Level null
         Terminal Session ✕
         Wait For Res
         Timeout (Seconds): 10
         Structure Output (JSON, etc): On ]
    
    A3: JavaScriptlet [
         Code: var uri = &#34;content://net.dinglisch.android.taskerm.fileprovider/storage/emulated/0/Documents/markor/foosel.github.io/&#34; + stdout.trim();
         Auto Exit: On
         Timeout (Seconds): 45 ]
    
    A4: Send Intent [
         Cat: None
         Mime Type: text/markdown
         Data: %uri
         Package: net.gsantner.markor
         Class: net.gsantner.markor.activity.DocumentActivity
         Target: Activity ]
</code></pre><h3 id="result">Result</h3>
<p>I now have all these tasks available under a tag in my launcher (<a href="https://kvaesitso.mm20.de/">Kvaesitso</a>) and can easily work on posts from my phone once more. In fact, this post was entirely written on it.</p>
<p><img src="https://foosel.net/til/2026-06-19-how-to-manage-this-page-from-my-phone-in-2026/shortcuts.png" alt="My launcher showing all created Tasker tasks as shortcuts, as well as a Firefox shortcut to the local page, plus Markor and Termux" loading="lazy">
</p>
<p>The whole pipeline is a bit slimmer than last time as well and required less bash wrappers, which I like a lot better 👍</p>
<p>I also decided to go a bit more commandline-centric in this iteration - <code>git add</code> and <code>git commit</code> I&rsquo;ll actually run manually in Termux this time around (I rarely used the corresponding shortcuts I created last time).</p>
<p>Once I&rsquo;m satisfied with the local preview and have committed, a <code>git push</code> is all that I need for publishing - the CI does the rest.</p>
]]></content:encoded></item><item><title>TIL: How to fix the Chromium WebHID connection for a Keychron G5 under Linux</title><link>https://foosel.net/til/2026-04-28-how-to-fix-the-webhid-connection-for-a-keychron-g5-under-linux/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2026-04-28-how-to-fix-the-webhid-connection-for-a-keychron-g5-under-linux/</guid><description>&lt;p&gt;Today my new mouse arrived, a &lt;a href="https://www.keychron.com/products/keychron-g5-ultra-light-wireless-mouse"&gt;Keychron G5&lt;/a&gt;. I had read that it was supposedly configurable through (Chromium based) browsers via &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API"&gt;WebHID&lt;/a&gt;, OS agnostic, so no need to install any weird (bloated) drivers or keep a Windows VM ready. However, when I went to &lt;code&gt;https://launcher.keychron.com&lt;/code&gt; in my ungoogled Chromium running under Fedora Linux, I had to realize that while it could see the mouse, it would not actually connect to it.&lt;/p&gt;
&lt;p&gt;A quick web search later I fell over &lt;a href="https://github.com/StefanMarAntonsson/keychron-launcher-arch-guide"&gt;this repository&lt;/a&gt; with a guide on how to get Chromium&amp;rsquo;s WebHID connection to work to a Keychron Q5 HE under ArchLinux, and I figured that was close enough (and the steps outlined therein logical enough) to adapt. And just so I can easily find this again, I&amp;rsquo;m replicating the crucial bits here&amp;hellip;&lt;/p&gt;</description><content:encoded><![CDATA[<p>Today my new mouse arrived, a <a href="https://www.keychron.com/products/keychron-g5-ultra-light-wireless-mouse">Keychron G5</a>. I had read that it was supposedly configurable through (Chromium based) browsers via <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebHID_API">WebHID</a>, OS agnostic, so no need to install any weird (bloated) drivers or keep a Windows VM ready. However, when I went to <code>https://launcher.keychron.com</code> in my ungoogled Chromium running under Fedora Linux, I had to realize that while it could see the mouse, it would not actually connect to it.</p>
<p>A quick web search later I fell over <a href="https://github.com/StefanMarAntonsson/keychron-launcher-arch-guide">this repository</a> with a guide on how to get Chromium&rsquo;s WebHID connection to work to a Keychron Q5 HE under ArchLinux, and I figured that was close enough (and the steps outlined therein logical enough) to adapt. And just so I can easily find this again, I&rsquo;m replicating the crucial bits here&hellip;</p>
<p>Apparently all I needed were some additional <code>udev</code> rules for both the mouse itself (when connected via the USB-C cable) as well as the 2.4GHz dongle (when connected wirelessly). So, first I figured out the relevant vendor and product IDs:</p>
<pre tabindex="0"><code>$ lsusb | grep -i keychron
Bus 005 Device 042: ID 3434:d028 Keychron Keychron Ultra-Link 8K
Bus 005 Device 062: ID 3434:d06f Keychron Keychron G5
</code></pre><p>Those turned out to be vendor <code>0x3434</code> and products <code>0xd06f</code> (wired mouse) and <code>0xd028</code> (dongle). So based on the repo linked above I created <code>/etc/udev/rules.d/50-keychron-g5.rules</code> as follows:</p>
<pre tabindex="0"><code># Keychron G5 (WebHID)
KERNEL==&#34;hidraw*&#34;, SUBSYSTEM==&#34;hidraw&#34;, ATTRS{idVendor}==&#34;3434&#34;, ATTRS{idProduct}==&#34;d06f&#34;, MODE=&#34;0666&#34;, TAG+=&#34;uaccess&#34;, TAG+=&#34;udev-acl&#34;
KERNEL==&#34;hidraw*&#34;, SUBSYSTEM==&#34;hidraw&#34;, ATTRS{idVendor}==&#34;3434&#34;, ATTRS{idProduct}==&#34;d028&#34;, MODE=&#34;0666&#34;, TAG+=&#34;uaccess&#34;, TAG+=&#34;udev-acl&#34;
</code></pre><p>then refreshed <code>udev</code>:</p>
<pre tabindex="0"><code>sudo udevadm control --reload-rules
sudo udevadm trigger
</code></pre><p>and replugged the mouse and restarted Chromium.</p>
<p>After that, the launcher page could finally connect! 🥳</p>
]]></content:encoded></item><item><title>TIL: How to automatically build a human.json file in Hugo</title><link>https://foosel.net/til/2026-03-20-how-to-automatically-build-a-humanjson-file-in-hugo/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2026-03-20-how-to-automatically-build-a-humanjson-file-in-hugo/</guid><description>&lt;p&gt;As I mentioned the other day, I recently added a &lt;a href="https://foosel.net/til/humanjson/"&gt;human.json file&lt;/a&gt; to this site.&lt;/p&gt;
&lt;p&gt;However, it became a bit annoying adding new vouches to the json manually.&lt;/p&gt;
&lt;p&gt;So today I sat down and made it get created automatically during my page build powered by &lt;a href="https://gohugo.io"&gt;Hugo&lt;/a&gt;, based on a list of vouches stored in a &lt;code&gt;vouches.yaml&lt;/code&gt; data file&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;For this I first created said data file in &lt;code&gt;data/vouches.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;url&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://food.foosel.net&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;date&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2026-03-14&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;url&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://chaos.social/@foosel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;date&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2026-03-14&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;url&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://octoprint.org&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;date&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2026-03-14&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I created a new output format &lt;code&gt;humanjson&lt;/code&gt; in my &lt;code&gt;config.yaml&lt;/code&gt;, making sure to also add it to the home page:&lt;/p&gt;</description><content:encoded><![CDATA[<p>As I mentioned the other day, I recently added a <a href="https://foosel.net/til/humanjson/">human.json file</a> to this site.</p>
<p>However, it became a bit annoying adding new vouches to the json manually.</p>
<p>So today I sat down and made it get created automatically during my page build powered by <a href="https://gohugo.io">Hugo</a>, based on a list of vouches stored in a <code>vouches.yaml</code> data file<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>For this I first created said data file in <code>data/vouches.yaml</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;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://food.foosel.net</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">date</span>: <span style="color:#e6db74">&#34;2026-03-14&#34;</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://chaos.social/@foosel</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">date</span>: <span style="color:#e6db74">&#34;2026-03-14&#34;</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://octoprint.org</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">date</span>: <span style="color:#e6db74">&#34;2026-03-14&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ...</span>
</span></span></code></pre></div><p>Then I created a new output format <code>humanjson</code> in my <code>config.yaml</code>, making sure to also add it to the home page:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">outputs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">home</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">HTML</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">JSON</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">humanjson</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">outputFormats</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">humanjson</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mediaType</span>: <span style="color:#ae81ff">application/json</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">baseName</span>: <span style="color:#ae81ff">human</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">isPlainText</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">notAlternative</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span></code></pre></div><p>Finally I created a template for that in  <code>_layout/home.humanjson.json</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;-webkit-text-size-adjust:none;"><code class="language-go-text-template" data-lang="go-text-template"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    &#34;version&#34;: &#34;0.1.1&#34;,
</span></span><span style="display:flex;"><span>    &#34;url&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.Site.BaseURL</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>    &#34;vouches&#34;: [
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">$index</span><span style="color:#f92672">,</span> <span style="color:#a6e22e">$vouch</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hugo</span><span style="color:#a6e22e">.Data.vouches</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">with</span> <span style="color:#a6e22e">$vouch</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">$index</span> <span style="color:#75715e">}}</span>,<span style="color:#75715e">{{</span><span style="color:#66d9ef">end</span><span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            &#34;url&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.url</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>,
</span></span><span style="display:flex;"><span>            &#34;vouched_at&#34;: <span style="color:#75715e">{{</span> <span style="color:#a6e22e">.date</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">jsonify</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">{{-</span> <span style="color:#66d9ef">end</span> <span style="color:#75715e">}}</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I kept the required <code>&lt;link rel=&quot;/human.json&quot;&gt;</code> in my <code>layouts/partials/extend_head.html</code> file that gets included on <em>all</em> pages by my theme (contrary to how the linking of output formats work, which is why I went with <code>notAlternative: true</code> for that).</p>
<p>And to make it even easier to quickly add a new vouch, I also added a new task to my <a href="https://taskfile.dev">Taskfile</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  <span style="color:#f92672">add-vouch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">desc</span>: <span style="color:#ae81ff">Adds a new vouch to human.json.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">PAGE</span>: <span style="color:#e6db74">&#39;{{.CLI_ARGS}}&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">FILE</span>: <span style="color:#e6db74">&#39;data/vouches.yaml&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">TODAY</span>: <span style="color:#e6db74">&#39;{{now | date &#34;2006-01-02&#34;}}&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cmds</span>:
</span></span><span style="display:flex;"><span>      - |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        cat &gt;&gt; {{.FILE}} &lt;&lt;EOF
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - url: {{.PAGE}}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          date: &#34;{{.TODAY}}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        EOF</span>
</span></span></code></pre></div><p>Now, vouching for a page<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> is as easy as</p>
<pre tabindex="0"><code>task add-vouch -- https://example.com
</code></pre><div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Targeting Hugo 0.156.0+&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Even from my phone thanks to <a href="https://termux.dev/">Termux</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>TIL: human.json</title><link>https://foosel.net/til/2026-03-15-humanjson/</link><pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2026-03-15-humanjson/</guid><description>&lt;p&gt;Thanks to &lt;a href="https://mastodon.social/@sethmlarson/116229292847984092"&gt;a toot by Seth Larson&lt;/a&gt; yesterday I learned about the &lt;a href="https://codeberg.org/robida/human.json"&gt;still in draft &lt;code&gt;human.json&lt;/code&gt; spec&lt;/a&gt; by Beto Dealmeida:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;human.json&lt;/code&gt; is a lightweight protocol for humans to assert authorship of their site content and vouch for the humanity of others. It uses URL ownership as identity, and trust propagates through a crawlable web of vouches between sites.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Basically, a web of trust to confirm that pages are written by actual human beings vs just LLM generated slop.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Thanks to <a href="https://mastodon.social/@sethmlarson/116229292847984092">a toot by Seth Larson</a> yesterday I learned about the <a href="https://codeberg.org/robida/human.json">still in draft <code>human.json</code> spec</a> by Beto Dealmeida:</p>
<blockquote>
<p><code>human.json</code> is a lightweight protocol for humans to assert authorship of their site content and vouch for the humanity of others. It uses URL ownership as identity, and trust propagates through a crawlable web of vouches between sites.</p>
</blockquote>
<p>Basically, a web of trust to confirm that pages are written by actual human beings vs just LLM generated slop.</p>
<p>That idea certainly clicked with me and so I quickly threw together a basic <a href="https://foosel.net/human.json"><code>human.json</code></a> in this page&rsquo;s <code>static</code> folder and linked to it from the <code>layouts/partials/extend_head.html</code> override file. That will get filled more over time 😊</p>
<p>And as reading through the spec of <code>human.json</code> also made me learn about the <a href="https://foosel.net/ai"><code>/ai</code> slashpage</a> I added that as well and linked it from the footer.</p>
<p>Those <a href="https://slashpages.net/">slashpages</a> are a fun idea I&rsquo;ll have to investigate further too!</p>
]]></content:encoded></item><item><title>TIL: How to work around tailscale breaking IPv6 on the host</title><link>https://foosel.net/til/2026-01-23-how-to-work-around-tailscale-breaking-ipv6-on-the-host/</link><pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2026-01-23-how-to-work-around-tailscale-breaking-ipv6-on-the-host/</guid><description>&lt;p&gt;Yesterday I ran into an incredibly weird issue. I installed the &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; client on two of my virtual servers hosted in the Hetzner Cloud (running Ubuntu) and suddenly the websites they offered stopped working. I suspected Tailscale and indeed, &lt;code&gt;tailscale down&lt;/code&gt; immediately restored functionality.&lt;/p&gt;
&lt;p&gt;The websites in question are actually on GitHub Pages and my servers are just acting as reverse proxy to resolve domain and TLS, and a look into the web server&amp;rsquo;s &lt;code&gt;error.log&lt;/code&gt; showed that the issue in serving was that the server could no longer reach its upstream at &lt;code&gt;github.io&lt;/code&gt; when Tailscale was active. It wasn&amp;rsquo;t a general loss of external connectivity though - IPv4 addresses still worked great, the webserver however was trying to connect to the upstream via IPv6 and this is where things failed.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Yesterday I ran into an incredibly weird issue. I installed the <a href="https://tailscale.com/">Tailscale</a> client on two of my virtual servers hosted in the Hetzner Cloud (running Ubuntu) and suddenly the websites they offered stopped working. I suspected Tailscale and indeed, <code>tailscale down</code> immediately restored functionality.</p>
<p>The websites in question are actually on GitHub Pages and my servers are just acting as reverse proxy to resolve domain and TLS, and a look into the web server&rsquo;s <code>error.log</code> showed that the issue in serving was that the server could no longer reach its upstream at <code>github.io</code> when Tailscale was active. It wasn&rsquo;t a general loss of external connectivity though - IPv4 addresses still worked great, the webserver however was trying to connect to the upstream via IPv6 and this is where things failed.</p>
<p>I did some quick tests, pinging Google&rsquo;s DNS on both IPv4 (<code>8.8.8.8</code>) and IPv6 (<code>2001:4860:4860::8888</code>) with Tailscale running, and that showed that while Tailscale was running, IPv6 connectivity just broke down completely while IPv4 continued to work:</p>
<pre tabindex="0"><code>$ sudo tailscale up &amp;&amp; ping -c 3 8.8.8.8 &amp;&amp; sudo tailscale down
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=3.98 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=3.69 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=117 time=3.63 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 3.631/3.771/3.986/0.162 ms

$ sudo tailscale up &amp;&amp; ping -c 3 2001:4860:4860::8888 &amp;&amp; sudo tailscale down
PING 2001:4860:4860::8888(2001:4860:4860::8888) 56 data bytes

--- 2001:4860:4860::8888 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2051ms
</code></pre><p>As a next step, I checked the route that would be taken for resolving <code>2001:4860:4860::8888</code> with and without Tailscale:</p>
<pre tabindex="0"><code>$ ip route get 2001:4860:4860::8888
2001:4860:4860::8888 from :: via fe80::1 dev eth0 src aaaa:bbbb:cccc:dddd::1 metric 1024 pref medium

$ sudo tailscale up &amp;&amp; ip route get 2001:4860:4860::8888 &amp;&amp; sudo tailscale down
2001:4860:4860::8888 from :: via fe80::1 dev eth0 src fd7a:115c:a1e0::aaaa:bbbb metric 1024 pref medium
</code></pre><p>So, the issue was that for some reason, while Tailscale was running the system decided to use Tailscale&rsquo;s internal IPv6 <code>fd7a:115c:a1e0::aaaa:bbbb</code> set on <code>tailscale0</code> as the source IP but send the packet through the default route and <code>eth0</code>, and that didn&rsquo;t work. It basically hijacked the IPv6 traffic, even when the Tailnet wasn&rsquo;t even involved.</p>
<p>The routes looked fine to me and some buddies I asked also didn&rsquo;t spot anything amiss:</p>
<pre tabindex="0"><code>$ ip -6 rule show
0:      from all lookup local
5210:   from all fwmark 0x80000/0xff0000 lookup main
5230:   from all fwmark 0x80000/0xff0000 lookup default
5250:   from all fwmark 0x80000/0xff0000 unreachable
5270:   from all lookup 52
32766:  from all lookup main

$ ip -6 route show table local
local ::1 dev lo proto kernel metric 0 pref medium
local aaaa:bbbb:cccc:dddd::1 dev eth0 proto kernel metric 0 pref medium
local fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 proto kernel metric 0 pref medium
local fe80::5fdc:58a7:1a93:da5a dev tailscale0 proto kernel metric 0 pref medium
local fe80::9400:ff:fe0d:61a1 dev eth0 proto kernel metric 0 pref medium
multicast ff00::/8 dev eth0 proto kernel metric 256 pref medium
multicast ff00::/8 dev tailscale0 proto kernel metric 256 pref medium

$ ip -6 route show table 52
fd7a:115c:a1e0::53 dev tailscale0 metric 1024 pref medium
fd7a:115c:a1e0::/48 dev tailscale0 metric 1024 pref medium

$ ip -6 route show table main
aaaa:bbbb:cccc:dddd::/64 dev eth0 proto kernel metric 256 pref medium
fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::1 dev eth0 metric 1024 pref medium
</code></pre><p>From what I could see, firing up Tailscale would add the following new rules to the routing table:</p>
<pre tabindex="0"><code>$ ip route show table all &gt; ts-off.txt
$ sudo tailscale up &amp;&amp; ip route show table all &gt; ts-on.txt &amp;&amp; sudo tailscale down
$ diff ts-off.txt ts-on.txt
0a1,2
&gt; 100.a.b.c dev tailscale0 table 52
&gt; 100.100.100.100 dev tailscale0 table 52
3a6
&gt; local 100.x.y.z dev tailscale0 table local proto kernel scope host src 100.x.y.z
12a16,17
&gt; fd7a:115c:a1e0::53 dev tailscale0 table 52 metric 1024 pref medium
&gt; fd7a:115c:a1e0::/48 dev tailscale0 table 52 metric 1024 pref medium
13a19
&gt; fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 proto kernel metric 256 pref medium
17a24
&gt; local fd7a:115c:a1e0::aaaa:bbbb dev tailscale0 table local proto kernel metric 0 pref medium
</code></pre><p>I still have not figured out what is actually going on there, and a reproduction on a fresh server so far also wasn&rsquo;t successful. The problem is that packets are being sent with the wrong source IPv6, but that&rsquo;s just a symptom of the underlying cause.</p>
<p>Thankfully, my buddy Jub came up with the workaround to change the default route to use a fixed IPv6 source address - the correct one - and that solved the issue (by fixing the symptom):</p>
<pre tabindex="0"><code>ip -6 route replace default via fe80::1 dev eth0 src aaaa:bbbb:cccc:dddd::1
</code></pre><p>I put that on a new <code>post-up</code> line into the network setup in <code>/etc/network/interface.d/50-cloud-init.cfg</code></p>
<pre tabindex="0"><code>auto eth0:0
iface eth0:0 inet6 static
    address aaaa:bbbb:cccc:dddd::/64
    gateway fe80::1
    post-up route add -net :: netmask 0 gw fe80::1%eth0 || true
    post-up ip -6 route replace default via fe80::1 dev eth0 src aaaa:bbbb:cccc:dddd::1 || true
    pre-down route del -net :: netmask 0 gw fe80::1%eth0 || true
</code></pre><p>A reboot confirmed that this works as a <strong>workaround</strong>.</p>
<p>But as I mentioned, I still can&rsquo;t make any sense of the underlying issue. I found <a href="https://github.com/tailscale/tailscale/issues/17936">one open bug report in Tailscale&rsquo;s bug tracker</a> that sounded familiar, but it didn&rsquo;t fully match my situation. I also have to admit that my administration skills kinda get a bit fuzzy when it comes to full blown route analysis &amp; debugging - so should you have any ideas at all what is actually causing this behaviour here, please get in touch <a href="https://chaos.social/@foosel">on Mastodon</a> - I&rsquo;d love to see this mystery solved, but am out of my depth here 😅</p>
]]></content:encoded></item><item><title>TIL: How to set the internal schedule on a Roomba 960 using rest980</title><link>https://foosel.net/til/2025-07-09-how-to-set-the-internal-schedule-on-a-roomba-960-using-rest980/</link><pubDate>Wed, 09 Jul 2025 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2025-07-09-how-to-set-the-internal-schedule-on-a-roomba-960-using-rest980/</guid><description>&lt;p&gt;For some reason my Roomba 960 decided to fall off the cloud, or at least the official app refuses to see it.&lt;/p&gt;
&lt;p&gt;I thankfully already have an instance of &lt;a href="https://github.com/koalazak/rest980"&gt;rest980&lt;/a&gt; running in my homelab anyhow, and it is still happily chatting with
the bot. And tbh, I might just block cloud access again as having everything local is better anyhow.&lt;/p&gt;
&lt;p&gt;In any case, I wanted to disable the schedule currently set on it internally to switch to scheduling stuff from my home automation,
but without the app working I wasn&amp;rsquo;t sure on how. So I went hunting through rest980&amp;rsquo;s source - as the README didn&amp;rsquo;t tell me what
I was looking for - and found that I could program the weekly schedule with some easy &lt;code&gt;curl&lt;/code&gt; magic via the &lt;code&gt;/api/local/config/week&lt;/code&gt;
endpoint.&lt;/p&gt;</description><content:encoded><![CDATA[<p>For some reason my Roomba 960 decided to fall off the cloud, or at least the official app refuses to see it.</p>
<p>I thankfully already have an instance of <a href="https://github.com/koalazak/rest980">rest980</a> running in my homelab anyhow, and it is still happily chatting with
the bot. And tbh, I might just block cloud access again as having everything local is better anyhow.</p>
<p>In any case, I wanted to disable the schedule currently set on it internally to switch to scheduling stuff from my home automation,
but without the app working I wasn&rsquo;t sure on how. So I went hunting through rest980&rsquo;s source - as the README didn&rsquo;t tell me what
I was looking for - and found that I could program the weekly schedule with some easy <code>curl</code> magic via the <code>/api/local/config/week</code>
endpoint.</p>
<p>Firing off a <code>GET</code> against that, this is the data structure I received:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;cycle&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;none&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;start&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;none&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;h&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">9</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">15</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">9</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;m&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>cycle</code> seems to be the on/off button from Sunday at index 0 to Saturday on index 6. <code>start</code> schedules a cleaning run, <code>none</code> disables it.
<code>h</code> is the hours on which to start each day, and <code>m</code> the minute.</p>
<p>What I wanted to do was to set all of the days to off, and this I achieved with this combined <code>GET</code>/<code>POST</code> call with some <code>jq</code> manipulation in the middle:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl $REST980_URL/api/local/config/week | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    jq <span style="color:#e6db74">&#39;.cycle[] = &#34;none&#34;&#39;</span> | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    curl --json @- $REST980_URL/api/local/config/week
</span></span></code></pre></div><p>Another problem - hopefully - solved! I&rsquo;ll see tomorrow if this <em>really</em> disabled the schedule 😅 but I&rsquo;m optimistic!</p>
]]></content:encoded></item><item><title>TIL: How to automatically sync screenshots from the Steamdeck to Immich</title><link>https://foosel.net/til/2025-03-25-how-to-automatically-sync-screenshots-from-the-steamdeck-to-immich/</link><pubDate>Tue, 25 Mar 2025 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2025-03-25-how-to-automatically-sync-screenshots-from-the-steamdeck-to-immich/</guid><description>&lt;p&gt;As part of &lt;a href="https://chaos.social/@foosel/114105591362840338"&gt;my ongoing effort to reduce my dependency on US services&lt;/a&gt;, I just moved my photos
from Google Photos to a self-hosted &lt;a href="https://immich.app/"&gt;immich&lt;/a&gt; instance (which I btw can only recommend so far).&lt;/p&gt;
&lt;p&gt;You might remember from &lt;a href="https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/"&gt;a previous TIL&lt;/a&gt;
that I had my Steamdeck configured to push my screenshots into a custom album on Google Photos. Obviously I had to change that now as well,
but sadly couldn&amp;rsquo;t use the existing &lt;a href="https://rclone.org/"&gt;rclone&lt;/a&gt;-based setup for it.&lt;/p&gt;</description><content:encoded><![CDATA[<p>As part of <a href="https://chaos.social/@foosel/114105591362840338">my ongoing effort to reduce my dependency on US services</a>, I just moved my photos
from Google Photos to a self-hosted <a href="https://immich.app/">immich</a> instance (which I btw can only recommend so far).</p>
<p>You might remember from <a href="https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/">a previous TIL</a>
that I had my Steamdeck configured to push my screenshots into a custom album on Google Photos. Obviously I had to change that now as well,
but sadly couldn&rsquo;t use the existing <a href="https://rclone.org/">rclone</a>-based setup for it.</p>
<p>My first idea was to utilize <a href="https://github.com/simulot/immich-go">immich-go</a>, as I have just successfully used that for the
three day long import of over 50000 pictures from my Google Photos takeout into immich. But that turned out to not be the right tool here: in order to not even try to
upload already existing files it will fetch an asset list from immich first, and while that really improves performance for large batch imports,
it takes way too long for uploading a single new screenshot.</p>
<p>So instead I went with something self-built which utilizes <a href="https://immich.app/docs/api/">immich&rsquo;s API</a>.</p>
<h2 id="a-custom-upload-script">A custom upload script</h2>
<p>The first part is this little bash script that will take a file as input, upload it to a pre-configured immich instance and also add it to a
pre-defined album (which already has to exist). This lives in <code>~/.local/bin/immich-upload.sh</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;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>IMMICH_SERVER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://immich.example.com&#34;</span>
</span></span><span style="display:flex;"><span>IMMICH_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;your api key goes here&#34;</span>
</span></span><span style="display:flex;"><span>IMMICH_ALBUM<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;your album id goes here&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>INPUT<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$INPUT<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;immich-upload.sh &lt;file&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>file_modified<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>stat -c %Y <span style="color:#e6db74">&#34;</span>$INPUT<span style="color:#e6db74">&#34;</span> | date --iso-8601<span style="color:#f92672">=</span>seconds<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>name<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>basename <span style="color:#e6db74">&#34;</span>$INPUT<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ---</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Uploading </span>$INPUT<span style="color:#e6db74"> to immich...&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>upload<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -sL --request POST <span style="color:#e6db74">&#34;</span>$IMMICH_SERVER<span style="color:#e6db74">/api/assets&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#e6db74">&#34;Content-Type: multipart/form-data&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#e6db74">&#34;Accept: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#e6db74">&#34;X-API-Key: </span>$IMMICH_KEY<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -F <span style="color:#e6db74">&#34;deviceId=\&#34;curl/steamdeck\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -F <span style="color:#e6db74">&#34;deviceAssetId=\&#34;</span>$name<span style="color:#e6db74">-</span>$file_modified<span style="color:#e6db74">\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -F <span style="color:#e6db74">&#34;fileCreatedAt=\&#34;</span>$file_modified<span style="color:#e6db74">\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -F <span style="color:#e6db74">&#34;fileModifiedAt=\&#34;</span>$file_modified<span style="color:#e6db74">\&#34;&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -F <span style="color:#e6db74">&#34;assetData=@\&#34;</span>$INPUT<span style="color:#e6db74">\&#34;&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>id<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span>$upload<span style="color:#e6db74">&#34;</span> | jq -r .id<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Uploaded file, asset id is </span>$id<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ---</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Adding file to album </span>$IMMICH_ALBUM<span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>payload<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>jq -n --arg id $id <span style="color:#e6db74">&#39;{ids:[$ARGS.named.id]}&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>album<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>curl -sL --request PUT <span style="color:#e6db74">&#34;</span>$IMMICH_SERVER<span style="color:#e6db74">/api/albums/</span>$IMMICH_ALBUM<span style="color:#e6db74">/assets&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#e6db74">&#34;Accept: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#e6db74">&#34;X-API-Key: </span>$IMMICH_KEY<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -d <span style="color:#e6db74">&#34;</span>$payload<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;... done&#34;</span>
</span></span></code></pre></div><h2 id="reacting-to-new-screenshots">Reacting to new screenshots</h2>
<p>I use <a href="https://github.com/watchexec/watchexec">watchexec</a> to listen for changes in my custom screenshot folder
(<a href="https://foosel.net/til/how-to-automatically-sync-screenshots-from-the-steamdeck-to-google-photos/">see this TIL post on how to set that up</a>)
and calling the upload script with the correct file name. I downloaded a release build of <code>watchexec</code> and threw it into <code>~/.local/bin</code>, then created another
script <code>~/.local/bin/sync-screenshots</code> that takes care of setting all of the correct parameters<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>WATCHEXEC<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.local/bin/watchexec&#34;</span>
</span></span><span style="display:flex;"><span>FOLDER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.steam_screenshots&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">${</span>WATCHEXEC<span style="color:#e6db74">}</span> --exts jpg,png,mp4 --fs-events create --emit-events-to environment -w $FOLDER -o queue -p -v -- <span style="color:#e6db74">&#39;/home/deck/.local/bin/immich-upload.sh &#34;$WATCHEXEC_COMMON_PATH/$WATCHEXEC_CREATED_PATH&#34;&#39;</span>
</span></span></code></pre></div><h2 id="putting-it-all-together">Putting it all together</h2>
<p>Finally, a new systemd unit in <code>~/.config/systemd/user/sync-screenshots.service</code> takes care of starting this bash script and keeping it running:</p>
<pre tabindex="0"><code>[Unit]
Description=Sync Steam Screenshots

[Service]
ExecStart=%h/.local/bin/sync-screenshots
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
</code></pre><p>I enabled and started that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl --user enable sync-screenshots
</span></span><span style="display:flex;"><span>systemctl --user start sync-screenshots
</span></span></code></pre></div><p>Then I took a screenshot and confirmed that the script had run:</p>
<pre tabindex="0"><code>Mar 25 15:17:03 steamdeck sync_screenshots[77135]: [Running: /home/deck/.local/bin/immich-upload.sh &#34;$WATCHEXEC_COMMON_PATH/$WATCHEXEC_CREATED_PATH&#34;]
Mar 25 15:17:03 steamdeck sync_screenshots[77188]: Uploading /home/deck/.steam_screenshots/7_20250325151703_1.png to immich...
Mar 25 15:17:04 steamdeck sync_screenshots[77188]: Uploaded file, asset id is 141dc605-edef-48f1-83b5-00bd9d72b13e
Mar 25 15:17:04 steamdeck sync_screenshots[77188]: Adding file to album 0ce35e68-a564-4e26-921e-c486cd9e4725...
Mar 25 15:17:05 steamdeck sync_screenshots[77188]: ... done
Mar 25 15:17:05 steamdeck sync_screenshots[77135]: [Command was successful]
</code></pre><p>And indeed, upon checking my immich instance, I was also looking at the freshly uploaded screenshot. Mission accomplished!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>It&rsquo;s currently reacting to newly added <code>jpg</code>, <code>png</code> or <code>mp4</code> files. The latter is in preparation of hopefully another toolchain to automatically convert clips from
<a href="https://store.steampowered.com/gamerecording">Steam&rsquo;s game recorder</a> that will automatically push its results into the screenshot folder as well, but that&rsquo;s only an idea for now.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>TIL: How to fix VirtualBox on Fedora 40 with Kernel 6.12+</title><link>https://foosel.net/til/2025-02-27-how-to-fix-virtualbox-on-fedora-40-with-kernel-612/</link><pubDate>Thu, 27 Feb 2025 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2025-02-27-how-to-fix-virtualbox-on-fedora-40-with-kernel-612/</guid><description>&lt;p&gt;I (accidentally&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;) did a software update on my laptop last night, and this morning when I needed my Win10 VM for something, VirtualBox threw an error like this at me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;VirtualBox can&amp;rsquo;t operate in VMX root mode. Please disable the KVM kernel extension, recompile your kernel and reboot (VERR_VMX_IN_VMX_ROOT_MODE).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A quick web search for &amp;ldquo;fedora update virtualbox vboxisomaker&amp;rdquo; gave me &lt;a href="https://discussion.fedoraproject.org/t/139896"&gt;this forum post&lt;/a&gt; and consequently
&lt;a href="https://www.virtualbox.org/ticket/22248"&gt;this bug report&lt;/a&gt;, in which I found the solution: I just had to add the kernel parameter &lt;code&gt;kvm.enable_virt_at_load=0&lt;/code&gt; to disable KVM -
which comes enabled by default since Kernel 6.12. I accomplished that with grubby:&lt;/p&gt;</description><content:encoded><![CDATA[<p>I (accidentally<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>) did a software update on my laptop last night, and this morning when I needed my Win10 VM for something, VirtualBox threw an error like this at me:</p>
<blockquote>
<p>VirtualBox can&rsquo;t operate in VMX root mode. Please disable the KVM kernel extension, recompile your kernel and reboot (VERR_VMX_IN_VMX_ROOT_MODE).</p>
</blockquote>
<p>A quick web search for &ldquo;fedora update virtualbox vboxisomaker&rdquo; gave me <a href="https://discussion.fedoraproject.org/t/139896">this forum post</a> and consequently
<a href="https://www.virtualbox.org/ticket/22248">this bug report</a>, in which I found the solution: I just had to add the kernel parameter <code>kvm.enable_virt_at_load=0</code> to disable KVM -
which comes enabled by default since Kernel 6.12. I accomplished that with grubby:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo grubby --update-kernel<span style="color:#f92672">=</span>ALL --args<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;kvm.enable_virt_at_load=0&#34;</span>
</span></span></code></pre></div><p>After a reboot, VirtualBox started again.</p>
<p>Given how often VirtualBox breaks for me on updates, long term I think I really need to find a different solution&hellip; And yes, I also really need to upgrade to Fedora 41, I know 😉</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I needed to a quick reboot before an online call and promptly forgot to uncheck &ldquo;install updates&rdquo; on the reboot dialog. Which made me be late on the call. Meh.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>TIL: How to fetch additional data for a flux query from a json file</title><link>https://foosel.net/til/2024-12-26-how-to-fetch-additional-data-for-a-flux-query-from-a-json-file/</link><pubDate>Thu, 26 Dec 2024 00:00:00 +0000</pubDate><guid>https://foosel.net/til/2024-12-26-how-to-fetch-additional-data-for-a-flux-query-from-a-json-file/</guid><description>&lt;p&gt;My buddy Romses is currently taking care of the &lt;a href="https://datagnome.de"&gt;Datenzwerg deployment&lt;/a&gt; at 38c3, and like at every event where we deploy them I&amp;rsquo;m updating our page and &lt;a href="https://grafana.datagnome.de"&gt;Grafana dashboard&lt;/a&gt; with the locations of the gnomes.&lt;/p&gt;
&lt;p&gt;So far the latter was always quite annoying: We have only the names of the gnomes in our influx data, and adding the location/deployment status to the graph thus meant having something like this for every single graph:&lt;/p&gt;</description><content:encoded><![CDATA[<p>My buddy Romses is currently taking care of the <a href="https://datagnome.de">Datenzwerg deployment</a> at 38c3, and like at every event where we deploy them I&rsquo;m updating our page and <a href="https://grafana.datagnome.de">Grafana dashboard</a> with the locations of the gnomes.</p>
<p>So far the latter was always quite annoying: We have only the names of the gnomes in our influx data, and adding the location/deployment status to the graph thus meant having something like this for every single graph:</p>
<pre tabindex="0"><code class="language-flux" data-lang="flux">import &#34;strings&#34;
import &#34;dict&#34;

locations = [
    &#34;Bashful&#34;: &#34;Uptime Bar&#34;,
    &#34;Dopey&#34;: &#34;c3cat&#34;,
    &#34;Grumpy&#34;: &#34;Späti&#34;,
    &#34;Happy&#34;: &#34;Kidspace&#34;,
    &#34;Hefty&#34;: &#34;HASS Assembly&#34;,
    &#34;Moopsy&#34;: &#34;Chaospost&#34;,
    &#34;Kinky&#34;: &#34;Eventphone&#34;,
    &#34;Nerdy&#34;: &#34;House of Tea&#34;,
    &#34;Sleepy&#34;: &#34;DDOS Bar&#34;,
    &#34;Sneezy&#34;: &#34;Wohnzimmer&#34;
]

from(bucket: &#34;datagnome&#34;)
  [...]
  |&gt; map(fn: (r) =&gt; ({r with device: r.device + &#34; (&#34; + dict.get(dict: locations, key: r.device, default: &#34;?&#34;) + &#34;)&#34;}))
</code></pre><p>Which of course means that I had to update this <code>locations</code> dict for every single panel, on every single deployment, at least twice (start and end of the event).</p>
<p>I finally decided I had to solve this differently and just now figured out how to keep the deployment info in <a href="https://github.com/romses/Datenzwerg/blob/main/docs/deployment.json">a JSON file on our git repo</a> and then querying <em>that</em> from the graphs, instead of manually keeping the lookup data updated in more than one place:</p>
<pre tabindex="0"><code class="language-flux" data-lang="flux">import &#34;strings&#34;
import &#34;dict&#34;
import &#34;http/requests&#34;
import &#34;experimental/json&#34;

response = requests.get(url: &#34;https://raw.githubusercontent.com/romses/Datenzwerg/refs/heads/main/docs/deployment.json&#34;)
data = json.parse(data: response.body)
locations = dict.fromList(pairs: data)

from(bucket: &#34;datagnome&#34;)
  [...]
  |&gt; map(fn: (r) =&gt; ({r with device: r.device + &#34; (&#34; + dict.get(dict: locations, key: r.device, default: &#34;?&#34;) + &#34;)&#34;}))
</code></pre><p>So far seems to work well, and I&rsquo;m very happy to be able to do this faster now (and also more easily from my phone, should I need to).</p>
<p>Next step: Figuring out how to use that JSON file to also keep the event box on the home page updated. But that&rsquo;s for another day :)</p>
]]></content:encoded></item></channel></rss>