<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>jasmin&#39;s little garden</title>
  <subtitle>The blog of a queer and chaotic person, rambling about tech and other stuff.</subtitle>
  <link href="https://jasminchen.dev/index.xml" rel="self" />
  <link href="https://jasminchen.dev/" />
  <updated>2026-08-15T10:49:09Z</updated>
  <id>https://jasminchen.dev/</id>
  <author>
    <name>nachtjasmin</name>
  </author>
  <entry>
    <title>Small collection of fish functions for working with the Hetzner Cloud</title>
    <link href="https://jasminchen.dev/notes/2026/fish-and-hcloud/" />
    <updated>2026-08-15T10:49:09Z</updated>
    <id>502b78cc553bde9f4c3224e6d6e9ef50d5ad5174</id>
    <content type="html">&lt;p&gt;Both in private and at work, I’m using the Hetzner Cloud CLI &lt;code&gt;hcloud&lt;/code&gt; extensively. The latter shouldn’t be surprising, given the fact that I’m working for them.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hcloud&lt;/code&gt; supports having multiple, so called &lt;em&gt;contexts&lt;/em&gt;. It makes it easier when working with multiple projects, since you can store multiple API tokens and project configurations locally. However, with each new context I’ve added, I felt the need to find a comfy solution for some workflows. Nothing an hour or two writing only shell scripts wouldn’t fix. :&amp;gt;&lt;/p&gt;
&lt;p&gt;Because I’m using &lt;a href=&quot;https://fishshell.com&quot;&gt;fish&lt;/a&gt;, the snippets below are also all written for that. That being said, it should be pretty easy to adjust them to your favorite shell, feel free to copy the ideas here.&lt;/p&gt;
&lt;h2 id=&quot;hcloud-token-print-the-current-token&quot;&gt;&lt;code&gt;hcloud-token&lt;/code&gt; – Print the current token&lt;/h2&gt;
&lt;p&gt;This is just a small helper and allows me to get the current token whenever I need it. For example, I can call tools that require the current Hetzner Cloud token via a &lt;code&gt;HCLOUD_TOKEN&lt;/code&gt; by prepending them with &lt;code&gt;HCLOUD_TOKEN=(hcloud-token)&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-fish&quot;&gt;&lt;code class=&quot;language-fish&quot;&gt;function hcloud-token --description &#39;Prints the current token&#39;
    hcloud config get token --allow-sensitive
end&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;hctx-interactive-context-chooser&quot;&gt;&lt;code&gt;hctx&lt;/code&gt; – interactive context chooser&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;hctx&lt;/code&gt; combines &lt;code&gt;hcloud&lt;/code&gt; with &lt;code&gt;fzf&lt;/code&gt; and allows me to set the current context interactively.
When I do not select any context, it’ll be unset.&lt;/p&gt;
&lt;pre class=&quot;language-fish&quot;&gt;&lt;code class=&quot;language-fish&quot;&gt;function hctx
    # `string trim` is necessary, because `hcloud` appends whitespace to each entry for the columnar layout,
    # even if there&#39;s only one column.
    set available (hcloud context list -o &#39;noheader&#39; -o &#39;columns=name&#39; | string trim)
    set chosen (printf &quot;%s&#92;n&quot; $available | fzf)
    or begin
        echo &#39;no context chosen, unsetting hcloud context...&#39;
        hcloud context unset
        return
    end

    hcloud context use $chosen
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While it’s a minor improvement, it would only be half as useful without the next function.&lt;/p&gt;
&lt;h2 id=&quot;fish-hcloud-prompt-prints-the-current-context-in-the-prompt&quot;&gt;&lt;code&gt;fish_hcloud_prompt&lt;/code&gt; – prints the current context in the prompt&lt;/h2&gt;
&lt;p&gt;What I love about fish, is that &lt;code&gt;fish_prompt&lt;/code&gt; is just a regular fish function. It’s one of the design choices that I really admire, since it makes extending the prompt quite easy. My &lt;code&gt;fish_prompt&lt;/code&gt; calls &lt;code&gt;fish_hcloud_prompt&lt;/code&gt;, which itself prints information about the current context. I won’t go into detail of the &lt;code&gt;fish_prompt&lt;/code&gt; configuration, you can find &lt;a href=&quot;https://git.sr.ht/~nachtjasmin/dotfiles/tree/main/item/private_dot_config/fish/functions/fish_prompt.fish&quot;&gt;my configuration on Sourcehut&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;fish_hcloud_prompt&lt;/code&gt; itself will print the name of the current context and the name of the project it’s residing in.
It’s using an undocumented API to query information about the project. If you click around in the UI with the DevTools of your browser being open, you can see quite a number of them, including the &lt;code&gt;/v1/_tokens/current&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;p&gt;There’s &lt;a href=&quot;https://github.com/hetznercloud/cli/issues/362&quot;&gt;closed issue #362 in the hcloud-cli&lt;/a&gt; requesting this feature, but given the lack of a public endpoint, I do get why it’s not implemented yet.&lt;/p&gt;
&lt;pre class=&quot;language-fish&quot;&gt;&lt;code class=&quot;language-fish&quot;&gt;function fish_hcloud_prompt
    # Skip the hcloud prompt if we don&#39;t have it installed.
    command -q hcloud; or return

    # Skip it if there&#39;s no active context.
    set -l context_name &quot;$(hcloud context active)&quot;
    test -n $context_name; or return

    # Prefix the prompt with the name of the context
    set -l hcloud_prompt &#39; (hcloud: &#39;
    set -a hcloud_prompt $context_name&#39;/&#39;

    # Add the name of the project
    set -l endpoint (hcloud config get endpoint)
    set -l path /_tokens/current
    set -a hcloud_prompt (curl -s $endpoint$path --header &quot;Authorization: Bearer $(hcloud-token)&quot; | jq -r &#39;.token.project.name&#39;)

    set -a hcloud_prompt &#39;)&#39;
    echo -n -s (set_color red)$hcloud_prompt(set_color normal)
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is by far the most recent and most useful addition to my prompt. It displays something like &lt;code&gt;(hcloud: masto/Mastodon)&lt;/code&gt;, displaying the name of the context and the actual name of the project I’m working on. This avoids me caring too much about the actual context names and displays them right in my shell.&lt;/p&gt;
&lt;p&gt;It might make sense to cache the output instead of invoking curl on every invocation of &lt;code&gt;fish_hcloud_prompt&lt;/code&gt;, otherwise you might run pretty quick into rate limits. I’d leave the implementation of that as an exercise for the reader. :p&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Fixing a FRITZ!Box 6660 Cable to get updates again</title>
    <link href="https://jasminchen.dev/notes/2026/fixing-a-fritz-box-6660-cable-to-get-updates-again/" />
    <updated>2026-08-09T08:55:47Z</updated>
    <id>31788f94a107236547b95981c4b9e604ec8c770e</id>
    <content type="html">&lt;p&gt;One week ago, I realised that the router at home wasn’t recieving updates. It’s a &lt;em&gt;FRITZ!Box 6660 Cable&lt;/em&gt; and I have to admit, I was &lt;em&gt;very&lt;/em&gt; confused about the fact that the device doesn’t receive any updates.
I knew that this was more common back in the days when it wasn’t possible to use arbitrary routers with your ISP. That practice was known as the famous &lt;em&gt;Routerzwang&lt;/em&gt; and it was forbidden a couple of years ago. I bought this router second hand for a few bucks and when I bought it back then, I didn’t think about it at all. I assumed I could use it and it would work fine. And it actually did, we we’re able to use it and replace the even shittier router that we got as part of the contract.&lt;/p&gt;
&lt;p&gt;At first, I thought that the device might have reached it’s end of life. A quick look at the product page however made me realise it’s well supported. But given that the device couldn’t find any updates and, additionally, I wasn’t able to provide my own update files via the web interface, I fell into a small rabbit hole.&lt;/p&gt;
&lt;p&gt;After all: The device I’m using is well-supported and therefore it should be possible to run a newer version of the firmware provided by the manufacturer, right? It’s not like I want to install a custom Linux system on it, no, I want to install a different firmware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Turns out: It’s possible.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The way to unlock the box, however, is a bit more complicated than I anticipated. In an ideal world, it would be the press of a button, together with some “I know what I’m doing” checkbox. I’m glad that I’m not the first one who’s trying to do this and hence, there is information out there on how to get it resolved.&lt;/p&gt;
&lt;h2 id=&quot;what-i-did&quot;&gt;What I did&lt;/h2&gt;
&lt;p&gt;After a bit of digging, I found the &lt;a href=&quot;https://bitbucket.org/fesc2000/ffritz/src/6591/README-6591.md&quot;&gt;ffritz repository on Bitbucket&lt;/a&gt;, with the &lt;code&gt;README-6591.md&lt;/code&gt; being the most important file for me.&lt;/p&gt;
&lt;p&gt;First, I tried to get more information about the firmware version is running. To do that, I had to download the “extended support information”, which is a huge &lt;code&gt;.txt&lt;/code&gt; file and do a search for &lt;code&gt;firmware_version&lt;/code&gt;. In my case, this returned:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firmware_version	avm
DMC	RTL=n,SL1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those are good news! Because the firmware version is &lt;code&gt;avm&lt;/code&gt;, I know this isn’t some box by some ISP, it’s a “regular” box. The only difference is that &lt;code&gt;RTL=n&lt;/code&gt; (RTL = retail) is set to no and I only have to override that setting. Apparently, this can be done via &lt;code&gt;ftp&lt;/code&gt;, which I think is some wild way of configuring the system. 😄&lt;/p&gt;
&lt;p&gt;To do so, I then set the IP of my notebook to the &lt;code&gt;192.168.178.2&lt;/code&gt;, set the gateway to the &lt;code&gt;192.168.178.1&lt;/code&gt; and the subnet mask to &lt;code&gt;255.255.255.0&lt;/code&gt; respectively. I connected it via the “LAN 1” port on the box, cut the power, waited a couple of seconds and then turned it back on. On my notebook, I opened &lt;code&gt;ftp&lt;/code&gt; and executed the following:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;ftp&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# opens the interactive FTP utility&lt;/span&gt;
ftp&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;open&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.178.1
Connected to &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.178.1 &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.178.1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;.
&lt;span class=&quot;token number&quot;&gt;220&lt;/span&gt; ADAM2 FTP Server ready
Name &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.178.1:jasmin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;: adam2
&lt;span class=&quot;token number&quot;&gt;331&lt;/span&gt; Password required &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; adam2
Password:
&lt;span class=&quot;token number&quot;&gt;230&lt;/span&gt; User adam2 successfully logged &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt;
Remote system &lt;span class=&quot;token builtin class-name&quot;&gt;type&lt;/span&gt; is AVM.
&lt;span class=&quot;token comment&quot;&gt;# Now, we set the DMC variable to mark this box as a proper retail box&lt;/span&gt;
ftp&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; quote SETENV DMC &lt;span class=&quot;token assign-left variable&quot;&gt;RTL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Y
&lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; SETENV &lt;span class=&quot;token builtin class-name&quot;&gt;command&lt;/span&gt; successful
&lt;span class=&quot;token comment&quot;&gt;# And this reboots it. That can take a while, I disconnected from the FTP client by pressing Ctrl+D.&lt;/span&gt;
ftp&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; quote REBOOT&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What’s important to know is that the &lt;code&gt;192.168.178.0/24&lt;/code&gt; subnet has to be chosen, irrespectively of your actual router settings. In our case, the device is usually reachable under the &lt;code&gt;192.168.188.1&lt;/code&gt; (a legacy from the past which wasn’t changed to the factory defaults so far), yet I was only able to reach it via the &lt;code&gt;192.168.178.1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After the reboot, it took a moment longer than I anticipated for the device to come up again, but after a minute or two, it was back again. And when logged in, I could finally navigate to the update screen and: it found a new update! My quest was done, we have a happy ending!&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://jasminchen.dev/notes/2026/fixing-a-fritz-box-6660-cable-to-get-updates-again/QBpLpjOwBx-854.avif 854w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://jasminchen.dev/notes/2026/fixing-a-fritz-box-6660-cable-to-get-updates-again/QBpLpjOwBx-854.webp 854w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://jasminchen.dev/notes/2026/fixing-a-fritz-box-6660-cable-to-get-updates-again/QBpLpjOwBx-854.png&quot; alt=&quot;The update screen of the box. It found a new OS update, after the last update was installed in 2024.&quot; width=&quot;854&quot; height=&quot;382&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Two years since the last update, however, jeez. I probably don’t want to know how many unpatched devices are out there. 🙈&lt;/p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.fuseboard.de/fritzbox.html&quot;&gt;http://www.fuseboard.de/fritzbox.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bitbucket.org/fesc2000/ffritz/src/6591/README-6591.md&quot;&gt;https://bitbucket.org/fesc2000/ffritz/src/6591/README-6591.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Writing (regularly) is hard</title>
    <link href="https://jasminchen.dev/notes/2026/writing-regularly-is-hard/" />
    <updated>2026-08-04T21:08:20Z</updated>
    <id>7d25b8d532ce07f66fda510149d2674942694353</id>
    <content type="html">&lt;p&gt;There’s nothing much to say, except that writing, whether it’s in a professional context or on thís blog, is just hard.
My brain isn’t used to it, words do not, unfortunately, just come out of my brain and onto the screen. I have lots of ideas of things to write about, yet, I don’t find the time to get it actually done. It surely doesn’t help either that I have to work eight hours a day, time solely dedicated to work and not this blog.&lt;/p&gt;
&lt;p&gt;It doesn’t matter if it’s documentation, a novel, or just a random &lt;code&gt;README.md&lt;/code&gt;. To those who have mastered this profession: &lt;strong&gt;You have my respect and I admire you.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Writing is, I think, a very underestimated skill. I’ve seen too many meeting notes that we’re incomplete or just a very very loose list of bullet points. No one understood them three weeks later, half of the discussed points were missing. Not to speak of the documentation that is halfway dead even before it was started. To get your thoughts sorted and to put them in a concise manner, that’s just &lt;em&gt;chef’s kiss&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It’s a skill that needs practice. A &lt;em&gt;lot&lt;/em&gt; of it. It’s a real bummer nonetheless. I want to write about things, yet I know it takes a lot of effort, because it’s hard, and so I don’t practice. welp. Guess, I am literally this meme:&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://jasminchen.dev/notes/2026/writing-regularly-is-hard/fLfuCI_nra-761.avif 761w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://jasminchen.dev/notes/2026/writing-regularly-is-hard/fLfuCI_nra-761.webp 761w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://jasminchen.dev/notes/2026/writing-regularly-is-hard/fLfuCI_nra-761.jpeg&quot; alt=&quot;The &amp;quot;no take, only throw&amp;quot; meme. It consists of three panels. In the first, there&#39;s a calm dog with a frisbee in it&#39;s mouth. It&#39;s captioned with &amp;quot;want to write better&amp;quot;. In the second panel, the mood shifts to angry. It&#39;s captioned with &amp;quot;no practice&amp;quot;. In the final panel, we get a close-up of the still angry dog, captioned with &amp;quot;just better&amp;quot;.&quot; width=&quot;761&quot; height=&quot;343&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Games I played recently</title>
    <link href="https://jasminchen.dev/notes/2026/games-i-played-this-year-so-far/" />
    <updated>2026-07-27T20:37:26Z</updated>
    <id>c2869ed64c59ccf3fc056d406132a24595d615dd</id>
    <content type="html">&lt;p&gt;Originally, I planned to write posts for each of them, but honestly, that feels more like a chore. Hence, this is just a quick summary of the games that I’ve played the past couple of months and what I think about them.&lt;/p&gt;
&lt;h2 id=&quot;split-fiction&quot;&gt;Split Fiction&lt;/h2&gt;
&lt;p&gt;I &lt;a href=&quot;https://jasminchen.dev/notes/2026/split-fiction/&quot;&gt;wrote about Split Fiction in May 2026&lt;/a&gt; and I’d still recommend it.&lt;/p&gt;
&lt;h2 id=&quot;assassin-s-creed-shadows&quot;&gt;Assassin’s Creed Shadows&lt;/h2&gt;
&lt;p&gt;I got this game as a Christmas gift in 2025. Since then, I’ve been occassionally playing it whenever I felt like it.
Because it’s 2026 and open world games are filled with &lt;strong&gt;a ton&lt;/strong&gt; of things to do, it’s not finished yet. And I already spent 90 hours in it.&lt;/p&gt;
&lt;p&gt;But I also have to say that I try to enjoy such games and grasp the open world. This means, that depending on my mood, I might me just wandering around there, slaughtering random enemies, riding the horse and discovering the world. I don’t have to rush it. If I remember correctly, I started my journey into the &lt;em&gt;Assassin’s Creed (AC)&lt;/em&gt; series with &lt;em&gt;Unity&lt;/em&gt;, the part that played in France, if I remember correctly. I got really into it with &lt;em&gt;AC: Origins&lt;/em&gt; (the part that played in Egypt). Ubisoft started to include more RPG elements with that part, going away a bit from the stealth game that it once was. Because it was their first attempt, it wasn’t really balanced. As a player, it was too easy after a while. Equipped with burning arrows, poisoning attacks and a burning sword, it was possible to kill everyone in a highly manned castle.&lt;/p&gt;
&lt;p&gt;I also played the games that came afterwards. &lt;em&gt;Assassin’s Creed Odyssey&lt;/em&gt;, playing in Greek (and which I really love because you can literally fuck around and be a whore lol), &lt;em&gt;Assassin’s Creed Valhalla&lt;/em&gt; (less gay, but still enjoyable to a certain extent) and now &lt;em&gt;Assassin’s Creed: Shadows&lt;/em&gt;. I skipped &lt;em&gt;Mirage&lt;/em&gt;, I was occupied with a different game at the time and it didn’t seem worth it.&lt;/p&gt;
&lt;p&gt;Anyway, back to &lt;em&gt;AC: Shadows&lt;/em&gt;. At first, I was worried about the open world. Sure, most of the time, game publishers can create a beautiful world, but that’s not worth anything if it feels dead. The worst example for me is &lt;em&gt;Tom Clancys: Ghost Recon Breakpoint&lt;/em&gt;. A massive open world, but apart from some enemies, it’s dead. You don’t see many civilians, birds or other critters. Just drones and enemies. (I got sidetracked again, didn’t I?)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Shadows&lt;/em&gt; however, doesn’t feel dead. It feels alive. There’s a certain kind of randomness to it and the game design really helped here. In previous games, the structure was hierarchical. You started with the easiest enemies and worked yourself up the ranks unless you can defeat the end boss. &lt;em&gt;Shadows&lt;/em&gt; took a different approach: Instead of having one hierarchical tree of enemies, you have smaller clusters of groups to defeat. And a lot of them, which are all discovered over time. And there are no hints about them early on, you just have to play the game! I really enjoyed that game design. It made the game feel less like a chore and more like an adventure.&lt;/p&gt;
&lt;p&gt;I also enjoy the character design and character switching. If you don’t know anything about the game: You can switch between Naoe (shinobi) and Yasuke (samurai) and depending on the situation one might be better suited for a task than the other. As I mentioned it earlier, you we’re quite overpowered in the game with all the utensils available for a fight &lt;em&gt;and&lt;/em&gt; the stealth tactics. &lt;em&gt;Shadows&lt;/em&gt; really forces you to play stealthy &lt;em&gt;or&lt;/em&gt; offensive. Naoe isn’t able to bow-hunt and Yasuke isn’t able to sneak or assassinate enemies. In the early hours of the game I died multiple times because I was caught and wasn’t able to defeat!&lt;/p&gt;
&lt;p&gt;There’s only thing I really hate about the game: Ubisoft should finally get rid of the Abstergo layer. They try to push it into there and each time it’s more disconnected. In &lt;em&gt;Shadows&lt;/em&gt;, there’s a “Anubis” menu where you can also buy stuff with Helix credits (wtf) and it’s one of the worst user experiences I can imagine out there. Even if I wanted to use it, I wouldn’t bear with that for one reason: &lt;strong&gt;It is fucking slow.&lt;/strong&gt; It’s like running in 5 frames per second on the PS5. The in game mechanics? Smooth as hell. Rendering a menu with 10 entries: slow as fuck. I really wonder how they achieved &lt;em&gt;that&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;need-for-speed-unbound&quot;&gt;Need for Speed: Unbound&lt;/h2&gt;
&lt;p&gt;A racing game for the moments my mind is humming “d-do do do, Max Verstappen”. It’s a &lt;em&gt;Need for Speed&lt;/em&gt;. Full of dad jokes, unrealistic driving mechanics and fast cars. I wouldn’t spend more than 20€ on it, but since I got it “for free” (as part of the PlayStation Plus abonnement), I can recommend it nonetheless. It’s really good for a quick gaming session in the afternoon, you really don’t have to think when playing it.&lt;/p&gt;
&lt;h2 id=&quot;star-wars-outlaws&quot;&gt;Star Wars: Outlaws&lt;/h2&gt;
&lt;p&gt;Another one of the games that I played only thanks to PlayStation Plus. It’s an open world game in the Star Wars universe. The best part of the game is &lt;em&gt;Nix&lt;/em&gt;, you pet companion, which you can pet all the time. In comparison, the main character, Kay, has a rather blunt character. Maybe I didn’t get the character design, but her dialogues felt… disconnected? I don’t know, it’s hard to describe. It felt like the people designing the character and story and the voice actors we’re never in the same room. That was, unfortunately, a common theme until the end.&lt;/p&gt;
&lt;p&gt;The end of the story didn’t make sense as well in my opinion. Do you know this moment when you watch a movie, which had a rather slow build up over 90 minutes and suddenly, in the last 20 minutes, everything gets resolved at once and there’s a happy ending?! That’s &lt;em&gt;Star Wars: Outlaws&lt;/em&gt; in a nutshell.&lt;/p&gt;
&lt;h2 id=&quot;life-is-strange-double-exposure&quot;&gt;Life is Strange: Double Exposure&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;gay gay homosexual gay. 11/10 much lesbianism, I approve.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No seriously, I can only recommend it. It’s dealing with a lot of negative mental health stuff, but luckily the game allows to enable content warnings &lt;strong&gt;on a case by case basis&lt;/strong&gt; in the menu. If I remember correctly, you have to set it right at startup, so you don’t get exposed without being warned.&lt;/p&gt;
&lt;p&gt;It also has cute lesbians. And older trans lesbians with leather jackets. Come on, play it. :&amp;gt;&lt;/p&gt;
&lt;p&gt;But be warned: You’ll cry. I’d &lt;strong&gt;love&lt;/strong&gt; to tell you more about it, but given the fact that I finished the game after 12 hours, I’m afraid that everything would be a spoiler. And the game doesn’t deserve that I spoil it to you.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>moo!</title>
    <link href="https://jasminchen.dev/notes/2026/moo/" />
    <updated>2026-07-01T21:24:06Z</updated>
    <id>00c37ce359ca43bb49dadaf47a8f2821f0931abf</id>
    <content type="html">&lt;p&gt;I don’t know why, but for some reason, &lt;a href=&quot;https://www.youtube.com/cow.txt&quot;&gt;YouTube has an ASCII cow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And I love that. I stole it. &lt;a href=&quot;https://jasminchen.dev/cow.txt&quot;&gt;It’s mine now.&lt;/a&gt; &amp;gt;:3&lt;/p&gt;
&lt;p&gt;moo! 🐄&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Building an image-based Debian 13 disk image using mkosi</title>
    <link href="https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/" />
    <updated>2026-07-02T11:32:31Z</updated>
    <id>7312fe5307417a23062812242bae8ba56da486c0</id>
    <content type="html">&lt;p&gt;mkosi, a project by the systemd folks, describes itself as a “fancy wrapper around &lt;code&gt;dnf --installroot&lt;/code&gt;, &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;pacman&lt;/code&gt; and &lt;code&gt;zypper&lt;/code&gt; that generates customized disk images with a number of bells and whistles”.&lt;/p&gt;
&lt;p&gt;What it basically does is: It bootstraps a system using the package manager of your choice, configures the partitions via &lt;code&gt;systemd-repart&lt;/code&gt; and at various steps in the process, you can hook into that. You can add additional files that should be part of the partitions, you can configure users, you can probably configure that system &lt;em&gt;exactly&lt;/em&gt; to your needs. If you’ve used &lt;a href=&quot;https://developer.hashicorp.com/packer&quot;&gt;Packer&lt;/a&gt; in the past, it’s similar to that. Except that it doesn’t take existing images, it builds entirely new ones.&lt;/p&gt;
&lt;p&gt;However, like all toolings out there, it has &lt;em&gt;opinions&lt;/em&gt;. Disk images are UEFI by default, the dependency on several systemd dependencies is, given the origin of the project, inherited and – and that’s probably the most unfortunate part – it doesn’t have that much documentation or blog posts explaining the concepts. On top of that, the configuration file &lt;code&gt;mkosi.conf&lt;/code&gt; changed a bit in the past couple of years and so older blog posts might not be actually adaptable for the current version, which is v25 as of writing this. (heck, I didn’t realise that I updated the &lt;a href=&quot;https://wiki.archlinux.org/index.php?title=Mkosi&amp;amp;oldid=838068&quot;&gt;mkosi documentation in the Arch Linux Wiki&lt;/a&gt; already a year ago, how long have I been postponing this blogpost?)&lt;/p&gt;
&lt;p&gt;This week, at the company-internal hackathon, me and &lt;a href=&quot;https://apricote.de&quot;&gt;my team lead Julian&lt;/a&gt; met and together, we tried to deploy a minimalistic &lt;em&gt;immutable&lt;/em&gt; Debian VM onto the Hetzner Cloud. It was an adventure and the crappy hotel wifi didn’t make it easier for us. But, after a while, we got it working. And so, this blogpost was born.&lt;/p&gt;
&lt;p&gt;(Disclaimer: I am employed by Hetzner Cloud. Opinions in this blogpost are my own and my employer has and had no influence about the contents in this post.)&lt;/p&gt;
&lt;h2 id=&quot;the-goal-debian-but-image-based&quot;&gt;The goal: Debian, but image-based&lt;/h2&gt;
&lt;p&gt;What we tried to achieve was the deployment of a minimal Debian system. In our case, minimal means: What is the absolute minimum of configuration required to deploy a working disk image that can be deployed as-is to the Hetzner Cloud? Because that would’ve been too easy, we also wanted it to be image-based (also commonly referred to as “immutable”).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;That means: &lt;code&gt;/usr&lt;/code&gt; is read-only and there’s no package manager to install additional toolings.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With this goal in mind, we started experimenting. The biggest hurdle was the fact that many of the less expensive server types still use a BIOS. For &lt;code&gt;mkosi&lt;/code&gt;, UEFI is required. From our experiences however, the &lt;code&gt;CPX&lt;/code&gt; models all seem to use UEFI by default. And so, we chose a &lt;code&gt;CPX22&lt;/code&gt; server for our experiments. For the rest of this post, I assume that you have &lt;code&gt;mkosi&lt;/code&gt; installed and a plain &lt;code&gt;mkosi&lt;/code&gt; invocation without any configuration or parameters works as intended on your system. (NixOS enjoyers, I’m particular looking at you. 😘)&lt;/p&gt;
&lt;h2 id=&quot;the-beginnings-preparing-the-cache&quot;&gt;The beginnings: Preparing the cache&lt;/h2&gt;
&lt;p&gt;One of the things that really help with speeding up builds is the existence of a &lt;code&gt;mkosi.cache&lt;/code&gt; directory. If it exists, package files downloaded as part of the process are cached in there. We also want to make sure that our created files do not clutter the project folder. If a &lt;code&gt;mkosi.output&lt;/code&gt; folder exists, &lt;code&gt;mkosi&lt;/code&gt; writes its files into that. So, without further ado:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; mkosi.&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;cache,output&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/mkosi.{cache,output}/&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; .gitignore&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;step-1-setting-up-our-mkosi-conf&quot;&gt;Step 1: Setting up our &lt;code&gt;mkosi.conf&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;We’ll be starting with a minimal &lt;code&gt;mkosi.conf&lt;/code&gt;. I set the &lt;code&gt;Timezone&lt;/code&gt;, &lt;code&gt;Keymap&lt;/code&gt; and &lt;code&gt;Locale&lt;/code&gt; explicitly instead of sticking to the defaults. Personally, I’m a fan of documenting things explicitly. That reduces the ambiguity when I haven’t looked at the configuration for a longer time.&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Output&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;disk&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Distribution&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Distribution&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;debian&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Release&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;trixie&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;mkosi-vm&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Bootable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;yes&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Timezone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;Europe/Berlin&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Keymap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;us&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Locale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;C.UTF-8&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Autologin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One of the most important lines is the &lt;code&gt;[Include]&lt;/code&gt; block. Without that, a bunch of packages aren’t installed. This includes the kernel, systemd and &lt;em&gt;a lot&lt;/em&gt; of other packages. If it isn’t included, &lt;code&gt;mkosi&lt;/code&gt; would be failing with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ mkosi
// &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;. omitted &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; brevity
‣ An EFI bootable image with systemd-boot was requested but a systemd-boot binary was not found at /usr/lib/systemd/boot/efi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we run that image via &lt;code&gt;mkosi vm&lt;/code&gt;, we have indeed a custom Debian image. Kudos to the mkosi maintainers for making it so easy. But hey, let’s make it more complex now. :&amp;gt;&lt;/p&gt;
&lt;h3 id=&quot;sidenote-debugging-the-configuration&quot;&gt;Sidenote: Debugging the configuration&lt;/h3&gt;
&lt;p&gt;Trying to understand what’s included in the current mkosi configuration and what isn’t is not that easy. Gladly, there’s the &lt;code&gt;mkosi cat-config&lt;/code&gt; command which also prints not only the current &lt;code&gt;mkosi.conf&lt;/code&gt;, but all the other upstream configuration files from the mkosi project as well.&lt;/p&gt;
&lt;h2 id=&quot;step-2-configuring-the-partitions&quot;&gt;Step 2: Configuring the partitions&lt;/h2&gt;
&lt;p&gt;If we have a look at the image that we’ve built, we see two partitions:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ mkosi
$ &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemd-dissect mkosi.output/image.raw
 File Name: image.raw
      Size: 1G
 Sec. Size: &lt;span class=&quot;token number&quot;&gt;512&lt;/span&gt;
     Arch.: x86-64
Image UUID: ffceab95-d3d2-4248-b198-ee60d799ab12
Image Name: image

OS Release: &lt;span class=&quot;token assign-left variable&quot;&gt;PRETTY_NAME&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Debian GNU/Linux &lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;trixie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token assign-left variable&quot;&gt;NAME&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Debian GNU/Linux
            &lt;span class=&quot;token assign-left variable&quot;&gt;VERSION_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt;
            &lt;span class=&quot;token assign-left variable&quot;&gt;VERSION&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;trixie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token assign-left variable&quot;&gt;VERSION_CODENAME&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;trixie
            &lt;span class=&quot;token assign-left variable&quot;&gt;DEBIAN_VERSION_FULL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;13.5&lt;/span&gt;
            &lt;span class=&quot;token assign-left variable&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;debian
            &lt;span class=&quot;token assign-left variable&quot;&gt;HOME_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://www.debian.org/
            &lt;span class=&quot;token assign-left variable&quot;&gt;SUPPORT_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://www.debian.org/support
            &lt;span class=&quot;token assign-left variable&quot;&gt;BUG_REPORT_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://bugs.debian.org/

    Use As: ✓ bootable system &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; UEFI
            ✓ bootable system &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; container
            ✗ portable &lt;span class=&quot;token function&quot;&gt;service&lt;/span&gt;
            ✗ initrd
            ✗ sysext &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; system
            ✗ sysext &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; portable &lt;span class=&quot;token function&quot;&gt;service&lt;/span&gt;
            ✗ sysext &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; initrd
            ✗ confext &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; system
            ✗ confext &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; portable &lt;span class=&quot;token function&quot;&gt;service&lt;/span&gt;
            ✗ confext &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; initrd

RW DESIGNATOR PARTITION UUID                       PARTITION LABEL FSTYPE ARCHITECTURE VERITY GROWFS PARTNO
rw root       b590b49b-09d4-4390-abf4-40ceb47748d5 root-x86-64     ext4   x86-64       no     &lt;span class=&quot;token function&quot;&gt;yes&lt;/span&gt;         &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
rw esp        b0641635-8d07-487f-a43c-f62598cab948 esp             vfat   -            -      no          &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our &lt;code&gt;root&lt;/code&gt; partition is &lt;code&gt;rw&lt;/code&gt; (read-write). That means that arbitrary processes can modify the rootfs and any other file in it. To change the partitions, we have to put partition definitions into the &lt;code&gt;mkosi.repart&lt;/code&gt; folder.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Beware:&lt;/strong&gt; An empty &lt;code&gt;mkosi.repart&lt;/code&gt; folder does not mean that the defaults are used, it means that you don’t want to have any partitions at all. We figured that out the hard way.&lt;/p&gt;
&lt;h3 id=&quot;esp-partition&quot;&gt;ESP partition&lt;/h3&gt;
&lt;p&gt;Just like in the current setup, we need two partitions: One for the esp/boot partition and the other one with the other system components of &lt;code&gt;/usr&lt;/code&gt;. The &lt;code&gt;mkosi.repart/00-esp.conf&lt;/code&gt; is defined as follows:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Partition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;esp&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;vfat&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;CopyFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/efi:/&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;CopyFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/boot:/&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;SizeMinBytes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;1G&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;SizeMaxBytes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;1G&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That creates a boot partition with a size of 1Gi, definitely large enough for anything we’re aiming to store in it. Depending on your requirements, you can reduce the size as well, but I personally discourage that. Increasing the size later on isn’t worth the effort to save like 500MB, unless you absolutely have to.&lt;/p&gt;
&lt;h3 id=&quot;erofs-for-usr&quot;&gt;erofs for &lt;code&gt;/usr&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;That being said, we also add a &lt;code&gt;/usr&lt;/code&gt; partition, defined in a &lt;code&gt;mkosi.repart/12-usr.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Partition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;usr&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;erofs&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;CopyFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/usr:/&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Minimize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;yes&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://en.wikipedia.org/wiki/EROFS&quot;&gt;EROFS filesystem&lt;/a&gt; is read-only by design, therefore we don’t have to mount it as read-only later on. It also means that it’s impossible to write stuff into, even if we (or an attacker) wanted to.&lt;/p&gt;
&lt;p&gt;Let’s build and start our system again with &lt;code&gt;mkosi vm -f&lt;/code&gt;. The system is starting until…&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[    **] Job dev-gpt&#92;x2dauto&#92;x2droot.device/start running (15s / 1min 30s)
// ...
[ TIME ] Timed out waiting for device dev-gpt&#92;x2dauto&#92;x2droot.device - /dev/gpt-auto-root.
[DEPEND] Dependency failed for initrd-root-device.target - Initrd Root Device.
[DEPEND] Dependency failed for sysroot.mount - Root Partition.
[DEPEND] Dependency failed for initrd-root-fs.target - Initrd Root File System.
[DEPEND] Dependency failed for systemd-pcrfs@sysroot.service - TPM PCR File System Measurement of /sysroot.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(btw, since this is starting QEMU under the hood: You can terminate the currently running VM by pressing &lt;kbd&gt;Ctrl-A&lt;/kbd&gt;+&lt;kbd&gt;X&lt;/kbd&gt;)&lt;/p&gt;
&lt;h3 id=&quot;root-tmpfs-to-the-rescue&quot;&gt;&lt;code&gt;root=tmpfs&lt;/code&gt; to the rescue?&lt;/h3&gt;
&lt;p&gt;Womp womp. Why is that? I &lt;em&gt;think&lt;/em&gt; (although I’m not 100% sure), that we need a &lt;code&gt;/&lt;/code&gt; root partition and we don’t have one. There are several ways we can provide one. One of them would be the &lt;code&gt;root=tmpfs&lt;/code&gt; kernel command line, which would then be interpreted by the &lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd-fstab-generator.html&quot;&gt;systemd-fstab-generator&lt;/a&gt;. So, let’s just do that.&lt;/p&gt;
&lt;pre class=&quot;language-diff-ini&quot;&gt;&lt;code class=&quot;language-diff-ini&quot;&gt;[Output]
Format=disk

[Distribution]
Distribution=debian
Release=trixie

[Include]
Include=mkosi-vm

[Content]
Bootable=yes
Timezone=Europe/Berlin
Keymap=us
Locale=C.UTF-8
KernelCommandLine=
&lt;span class=&quot;token inserted-sign inserted language-ini&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token key attr-name&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;tmpfs&lt;/span&gt;
&lt;/span&gt;Autologin=true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Building and starting the VM will now stop at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         Starting initrd-switch-root.service - Switch Root...
[FAILED] Failed to start initrd-switch-root.service - Switch Root.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And if we look into the status by executing &lt;code&gt;systemctl status initrd-switch-root.service&lt;/code&gt;, we get the following information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Failed to switch root: Specified switch root path &#39;/sysroot&#39; does not seem to be an OS tree. os-release file is missing.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;sighs.&lt;/em&gt; I have to admit, this is confusing at hell. Because, yeah, it’s right: &lt;code&gt;ls -l /sysroot | wc -l&lt;/code&gt; returns 0. The folder is empty. However, &lt;code&gt;/usr/lib/os-release&lt;/code&gt; actually contains the files we need. I am actually wondering where the &lt;code&gt;/sysroot&lt;/code&gt; is coming from. (Update 2026-07-02: It is the default folder of &lt;code&gt;systemctl switch-root&lt;/code&gt;.) Since the &lt;code&gt;systemd-fstab-generator&lt;/code&gt; mentions the &lt;code&gt;mount.usr=dissect&lt;/code&gt; boot flag as well, we added this. Did it help? No. This time, &lt;code&gt;sysusr-usr.mount&lt;/code&gt; failed with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount: /sysusr/usr: fsconfig() failed: dissect: Can&#39;t lookup blockdev.
       dmesg(1) may have more information after failed mount system call.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We first assumed, that &lt;code&gt;systemd-dissect&lt;/code&gt; was missing in the initrd. But, as it turns out, it wasn’t that. &lt;code&gt;/dev/vda&lt;/code&gt; gets mounted, &lt;code&gt;dmesg&lt;/code&gt; doesn’t seem to report any errors as well. So, what’s going on? Turns out, &lt;code&gt;mount.usr=dissect&lt;/code&gt; is not supported by systemd v257 (which is the current version in Debian 13). When looking at the documentation, the current version displayed is systemd v260. &lt;strong&gt;We we’re looking at the wrong version of the documentation.&lt;/strong&gt; And so, &lt;code&gt;mount&lt;/code&gt; tried to find a device named &lt;code&gt;dissect&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;How can we circumvent it? Well, since &lt;code&gt;mount.usr&lt;/code&gt; takes in the arbitrary arguments one can pass to &lt;code&gt;mount&lt;/code&gt;, we can add a partition identifier to let it discover our &lt;code&gt;/usr&lt;/code&gt; partition in a different way. For example, by passing a UUID of the partition. When looking at the docs of mkosi, it even says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Otherwise, if the value of this setting contains the literals root=PARTUUID or mount.usr=PARTUUID, these are replaced with the partition UUID of the root or usr partition respectively.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, without further ado, let’s do that:&lt;/p&gt;
&lt;pre class=&quot;language-diff-ini&quot;&gt;&lt;code class=&quot;language-diff-ini&quot;&gt;# mkosi.conf

KernelCommandLine=
&lt;span class=&quot;token unchanged language-ini&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;   &lt;span class=&quot;token key attr-name&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;tmpfs&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;token inserted-sign inserted language-ini&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token key attr-name&quot;&gt;mount.usr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;PARTUUID&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now it works! Running &lt;code&gt;mkosi vm&lt;/code&gt; will now also trigger the &lt;code&gt;systemd-firstboot.service&lt;/code&gt;, which allows us to enter a default root password. For now, let’s just keep it empty and press &lt;kbd&gt;Enter&lt;/kbd&gt; twice.&lt;/p&gt;
&lt;h2 id=&quot;moving-stuff-from-etc-to-usr-share-factory-etc&quot;&gt;Moving stuff from &lt;code&gt;/etc&lt;/code&gt; to &lt;code&gt;/usr/share/factory/etc&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The system runs, but fails with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;login: PAM failure, aborting: Critical error - immediate abort
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why? There’s a bunch of files that we’re installed in &lt;code&gt;/etc&lt;/code&gt;. But we don’t copy any of these files over, we just take the &lt;code&gt;/usr&lt;/code&gt; partition as is. &lt;code&gt;/etc&lt;/code&gt; is empty. That means we’re probably missing some crucial configuration files that are required by the PAM stack. And, indeed, when looking at the &lt;a href=&quot;https://github.com/systemd/particleos/blob/9e3fc044fe6ce88fe8049b6f2c7157636994e9ea/mkosi.postinst.chroot&quot;&gt;&lt;code&gt;mkosi.postinst.chroot&lt;/code&gt; script in the ParticleOS repository&lt;/a&gt;, it mentions fixes for the PAM stack.&lt;/p&gt;
&lt;p&gt;But that’s not the only important script we should copy from that repository. The &lt;code&gt;mkosi.finalize&lt;/code&gt; script is equally important! It copies all of &lt;code&gt;/etc&lt;/code&gt; to &lt;code&gt;/usr/share/factory/etc&lt;/code&gt;. You might wonder, how it all will be copied back into &lt;code&gt;/etc&lt;/code&gt;? That’s done by &lt;code&gt;systemd-tmpfiles&lt;/code&gt;, the &lt;a href=&quot;https://github.com/systemd/particleos/blob/9e3fc044fe6ce88fe8049b6f2c7157636994e9ea/mkosi.extra/usr/lib/tmpfiles.d/etc.conf&quot;&gt;&lt;code&gt;etc.conf&lt;/code&gt; has quite a lot of symlinks&lt;/a&gt;. So, let’s copy those over as well. After it’s done, there should be three new files in the same folder as the &lt;code&gt;mkosi.conf&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mkosi.extra/usr/lib/tmpfiles.d/etc.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkosi.finalize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkosi.postinst.chroot&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unfortunately, this didn’t fix the PAM errors for me. After hours of experimenting and comparing it with an existing Debian 13 VM, I stumbled across &lt;a href=&quot;https://github.com/systemd/particleos/pull/84&quot;&gt;PR #84 in the ParticleOS repo&lt;/a&gt;, which mentioned a Debian-specific fixes. That’s how I found the &lt;a href=&quot;https://github.com/systemd/particleos/blob/9e3fc044fe6ce88fe8049b6f2c7157636994e9ea/mkosi.conf.d/debian/mkosi.extra/usr/lib/tmpfiles.d/etc-debian.conf&quot;&gt;&lt;code&gt;etc-debian.conf&lt;/code&gt;&lt;/a&gt;. Adding it next to the existing &lt;code&gt;tmpfiles.d&lt;/code&gt; definition and rebuilding the VM fixed the issue! 🎉&lt;/p&gt;
&lt;p&gt;(Note to the mkosi/particleos maintainers: As someone who isn’t working with mkosi everyday, it’s not that easy to figure out where to search for the configuration snippets. Sometimes it’s part of mkosi itself, sometimes the fixes for solutions can be found in the particleos repository. It’s a lot of trial and error. 😕)&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/6oFudvLEsl-1000.avif 1000w, https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/6oFudvLEsl-1392.avif 1392w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/6oFudvLEsl-1000.webp 1000w, https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/6oFudvLEsl-1392.webp 1392w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/6oFudvLEsl-1000.png&quot; alt=&quot;The Hetzner Cloud console window showing the lsblk output along with a bunch of other failed commands.&quot; width=&quot;1392&quot; height=&quot;1063&quot; srcset=&quot;https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/6oFudvLEsl-1000.png 1000w, https://jasminchen.dev/notes/2026/experimenting-with-mkosi-and-debian/6oFudvLEsl-1392.png 1392w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Of course, it’s very basic. Depending on what you’re trying to achieve, I think it makes sense to install &lt;code&gt;cloud-init&lt;/code&gt; and &lt;code&gt;qemu-guest-agent&lt;/code&gt; into the VM as well. Remember: You can’t install things via &lt;code&gt;apt&lt;/code&gt;, if something is missing like &lt;code&gt;fdisk&lt;/code&gt;, debugging becomes a lot harder.&lt;/p&gt;
&lt;h2 id=&quot;uploading-the-image-to-hetzner&quot;&gt;Uploading the image to Hetzner&lt;/h2&gt;
&lt;p&gt;Julian’s &lt;a href=&quot;https://github.com/apricote/hcloud-upload-image&quot;&gt;hcloud-upload-image tool&lt;/a&gt; has a &lt;a href=&quot;https://github.com/apricote/hcloud-upload-image/pull/178&quot;&gt;&lt;s&gt;WIP&lt;/s&gt; merged(!) implementation of a &lt;code&gt;hcloud-upload-image write-to-disk&lt;/code&gt; command&lt;/a&gt;. So, let’s just use that:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;HCLOUD_TOKEN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hcloud config get token --allow-sensitive&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
$ &lt;span class=&quot;token comment&quot;&gt;# Build a compressed disk image to reduce the amount of bytes that have to be transfered.&lt;/span&gt;
$ mkosi build --compress-output&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;yes
$ hcloud-upload-image write-to-disk &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--server&lt;/span&gt; mkosi-exp &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    --image-path mkosi.output/image.raw.zst &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--compression&lt;/span&gt; zstd&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, if you don’t want to use the tool, you can also do something along the lines of:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;no &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;UserKnownHostsFile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/dev/null &lt;span class=&quot;token string&quot;&gt;&#39;zstd -cd | dd status=progress of=/dev/sda bs=4M conv=sparse&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; mkosi.output/image.raw.zst&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;recap&quot;&gt;Recap&lt;/h2&gt;
&lt;p&gt;Let’s recap what we’ve build so far. We have a Debian 13 disk image, which contains the necessary (and some extra quality-of-life) components to run the system. &lt;code&gt;apt&lt;/code&gt; and other package managers are not part of the system. The whole system is bootstrapped out of a read-only &lt;code&gt;/usr&lt;/code&gt; partition. Due to &lt;code&gt;/&lt;/code&gt; being a &lt;code&gt;tmpfs&lt;/code&gt;, all state is lost on reboot.&lt;/p&gt;
&lt;p&gt;I can imagine this scenario being already useful for things like ephemeral CI runners or DNS servers. And for servers that run longer than a workday, the concept of &lt;em&gt;&lt;a href=&quot;https://grahamc.com/blog/erase-your-darlings/&quot;&gt;erasing your darlings&lt;/a&gt;&lt;/em&gt; has it’s benefits as well.&lt;/p&gt;
&lt;p&gt;If however, it’s a goal to manage systems that have state and store said state locally, I’d argue to look into &lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/repart.d.html&quot;&gt;repart.d&lt;/a&gt; and the &lt;a href=&quot;https://github.com/systemd/particleos/tree/main/mkosi.extra/usr/lib/repart.d&quot;&gt;repart.d files by the ParticleOS repository&lt;/a&gt;. With those, you can easily bootstrap any number of partitions (swap, state, home, etc.) declaratively. I, for my part, will also look into the &lt;em&gt;Unified Kernel Images&lt;/em&gt; (UKI) options more.&lt;/p&gt;
&lt;p&gt;And, to make it easier for you to start working on it: I’ve prepared a Git repository containing all the files in place. You can find the &lt;a href=&quot;https://codeberg.org/nachtjasmin/mkosi-debian-experiment&quot;&gt;nachtjasmin/mkosi-debian-experiment on Codeberg&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Happy hacking!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Using Apache2/httpd instead of Caddy</title>
    <link href="https://jasminchen.dev/notes/2026/using-apache2-httpd-instead-of-caddy/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>2450b24df0bea57346c1744738391d6feae91530</id>
    <content type="html">&lt;p&gt;In the beginnings, this blog was hosted on &lt;a href=&quot;https://uberspace.de&quot;&gt;Uberspace&lt;/a&gt;. Because of that, it was served by the httpd server, also known as Apache2. After migrating it to my own server, it was served by Caddy instead.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Caddyfile&lt;/code&gt; I used to serve this homepage was rather simple:&lt;/p&gt;
&lt;pre class=&quot;language-caddyfile&quot;&gt;&lt;code class=&quot;language-caddyfile&quot;&gt;https://jasminchen.dev {
    root * /usr/share/caddy
    file_server
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, it came with a caveat, which I only realised recently. &lt;strong&gt;Caddy doesn’t support the &lt;code&gt;.htaccess&lt;/code&gt; files by httpd.&lt;/strong&gt; This wouldn’t be an issue for most things, but I was using that for redirects of old URIs. And since I’m convinced that &lt;a href=&quot;https://www.w3.org/Provider/Style/URI&quot;&gt;cool URIs don’t change&lt;/a&gt;, I wanted to fix that.&lt;/p&gt;
&lt;p&gt;The maintenance of my redirects is done via an &lt;code&gt;aliases&lt;/code&gt; entry in the frontmatter and a Nunjucks template &lt;code&gt;htaccess.njk&lt;/code&gt;, which iterates over all pages and writes &lt;code&gt;Redirect 301&lt;/code&gt; entries to a &lt;code&gt;.htaccess&lt;/code&gt; file.
I haven’t found a suitable “Caddy-native” alternative which didn’t feel like a hack. I also don’t want to maintain a custom configuration snippet and reload that every time I deploy my homepage. And since it’s just a static homepage, I don’t have any special requirements at all. I just need a web server that serves a bunch of HTML and CSS.&lt;/p&gt;
&lt;p&gt;Given that I use Fedora as my server OS, httpd was installed pretty quickly via &lt;code&gt;dnf install httpd mod_md&lt;/code&gt;. &lt;code&gt;mod_md&lt;/code&gt; is the module that provides ACME support for Apache out of the box, so I don’t need certbot/acme.sh/… as well.
The configuration file that I placed in &lt;code&gt;/etc/httpd/conf.d/homepage.conf&lt;/code&gt; is a bit longer than the &lt;code&gt;Caddyfile&lt;/code&gt; above, but I still think it’s pretty concise:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MDomain jasminchen.dev
MDCertificateAgreement accepted
MDContactEmail acme@example.com

# Redirect all http to https, automatically sets the HSTS header.
MDRequireHttps permanent

&amp;lt;Directory /var/www/html&amp;gt;
    Options Indexes
    # Enable .htaccess files
    AllowOverride All
    Require all granted
&amp;lt;/Directory&amp;gt;

&amp;lt;VirtualHost *:443&amp;gt;
    ServerName jasminchen.dev
    DocumentRoot /var/www/html/

    SSLEngine on
    # Enable HTTP2
    Protocols h2 http/1.1
&amp;lt;/VirtualHost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, all my redirects for old URLs are working again, yippie! :3&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Converting a systemd-homed user to a regular one</title>
    <link href="https://jasminchen.dev/notes/2026/converting-a-systemd-homed-user-to-a-regular-one/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>f14a6c01252a91ab9048b93d69ef4ff5c37b75e4</id>
    <content type="html">&lt;p&gt;When I set up our homeserver, I tried out the newest kid on the block for user management on systemd-based distros: &lt;a href=&quot;https://wiki.archlinux.org/title/Systemd-homed&quot;&gt;systemd-homed&lt;/a&gt;. I no longer remember why I chose it, I guess it was just the regular “oooh, shiny, want to play with it”.&lt;/p&gt;
&lt;p&gt;Yet, the fact that I wasn’t able to just &lt;code&gt;rsync&lt;/code&gt; anything onto the remote server easily without &lt;em&gt;activating&lt;/em&gt; the session first by logging in via a separate SSH session annoys me. Therefore I decided to ditch the homed approach and replace my user with a regular one.&lt;/p&gt;
&lt;p&gt;To do so, I performed the following steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Stop the current session: &lt;code&gt;homectl deactivate jasmin&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a backup of the homedir by executing &lt;code&gt;sudo rsync -aHAUXv /home/jasmin.homedir/ /home/jasmin.homedir.bak/&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Remove the user via &lt;code&gt;homectl remove jasmin&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create the new user using &lt;code&gt;sudo useradd -G wheel -m -s /usr/bin/fish jasmin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This automatically adds me to the &lt;code&gt;wheel&lt;/code&gt; group for the sudoers, sets my shell to &lt;a href=&quot;https://fishshell.com/&quot;&gt;fish&lt;/a&gt; and creates a new home directory at &lt;code&gt;/home/jasmin&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Restore the backup with &lt;code&gt;sudo rsync -aHAUXv --chown jasmin:jasmin /home/jasmin.homedir.bak/ /home/jasmin/&lt;/code&gt;.
Since the old files were owned by &lt;code&gt;nobody:nobody&lt;/code&gt;, the &lt;code&gt;--chown jasmin:jasmin&lt;/code&gt; ensures that the new user actually owns their files.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And voilá, now I converted my homed user into a regular one.&lt;/p&gt;
&lt;p&gt;In retrospect, I should’ve specified the UID of the new user to be the old one. I converted the old files and folders that belonged to me afterwards by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Starts a transient systemd unit to find all directories&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# in /data that belonged to the UID 60424 (my previous one) and&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# changes the owner of them to the new user and group.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# fd is: https://github.com/sharkdp/fd&lt;/span&gt;
systemd-run -- fd &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt; /data &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60424&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-tdirectory&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; jasmin:jasmin&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/data&lt;/code&gt; is the root directory of the ZFS pool. Of course, you can run it for other folders as well, I also ran it for &lt;code&gt;/etc&lt;/code&gt; and &lt;code&gt;/var&lt;/code&gt;, just to be sure.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>I blame the hyperscalers for the IPv4-first mindset</title>
    <link href="https://jasminchen.dev/notes/2026/hyperscalers-ipv4-first-mindset/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>410b05bd7ba53db261aee5bd234cb72f524272ac</id>
    <content type="html">&lt;p&gt;For reasons, I’m experimenting a bit with Kubernetes again. And, since I planned at one point in 2025 to stop using IPv4
(or better said: dual stack), I wanted to set up Kubernetes in an IPv6-only mode.&lt;/p&gt;
&lt;p&gt;Kubernetes does support it. It’s supported out of the box. And yet, if you’re not using &lt;code&gt;kubeadm&lt;/code&gt; but instead want to take an easier approach using any of the “small” variants (k3s, k0s, etc.), it seems like no one thought about IPv6. And I would be &lt;em&gt;fine&lt;/em&gt; with that if it’s just an issue in early versions of the toolings.&lt;/p&gt;
&lt;p&gt;But no, it’s not. I setup a new Hetzner VM without an IPv4 address. Just to figure out what’s possible.&lt;/p&gt;
&lt;h2 id=&quot;first-attempt-k0s&quot;&gt;First attempt: k0s&lt;/h2&gt;
&lt;p&gt;Given the &lt;code&gt;curl | sh&lt;/code&gt; workflow, I followed their setup guide and did a&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--proto&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;=https&#39;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--tlsv1.2&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sSf&lt;/span&gt; https://get.k0s.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Guess what’s not available via IPv6? Right, &lt;code&gt;get.k0s.sh&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;second-attempt-k3sup&quot;&gt;Second attempt: k3sup&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alexellis/k3sup&quot;&gt;k3sup&lt;/a&gt; is just like &lt;a href=&quot;https://k3s.io/&quot;&gt;k3s&lt;/a&gt;, but connects via SSH to the remote server. Different approach than with k0s, but hey, I’m experimenting. What I got instead?&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; k3sup &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--ip&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;2a01:4f9:c013:b1c9::1&#39;&lt;/span&gt; --ssh-key ~/.ssh/id_ed25519
Running: k3sup &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;2026&lt;/span&gt;/05/20 &lt;span class=&quot;token number&quot;&gt;23&lt;/span&gt;:35:03 2a01:4f9:c013:b1c9::1
Public IP: 2a01:4f9:c013:b1c9::1
Error: unable to connect to 2a01:4f9:c013:b1c9::1:22 over ssh: dial tcp: address 2a01:4f9:c013:b1c9::1:22: too many colons &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; address&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fucking christ. But hey, we can wrap the IP address in brackets, that will fix it, right?&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; k3sup &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--ip&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;[2a01:4f9:c013:b1c9::1]&#39;&lt;/span&gt; --ssh-key ~/.ssh/id_ed25519
Error: invalid argument &lt;span class=&quot;token string&quot;&gt;&quot;[2a01:4f9:c013:b1c9::1]&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;--ip&quot;&lt;/span&gt; flag: failed to parse IP: &lt;span class=&quot;token string&quot;&gt;&quot;[2a01:4f9:c013:b1c9::1]&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;😭&lt;/p&gt;
&lt;p&gt;come on, pls. &lt;a href=&quot;https://github.com/alexellis/k3sup/issues/398&quot;&gt;The issue for IPv6 support&lt;/a&gt; is 3,5 years old.
The patch for that is a &lt;code&gt;+9 -6&lt;/code&gt;. It’s Go. Instead of concatenating strings, just use &lt;code&gt;net.JoinHostPort&lt;/code&gt; and voila, you have IPv6 support.&lt;/p&gt;
&lt;h2 id=&quot;the-elephant-in-the-room-github&quot;&gt;The elephant in the room: GitHub&lt;/h2&gt;
&lt;p&gt;GitHub is still IPv4-only. And I doubt it’ll ever change. But hey, at least they’re working hard to become irrelevant, one downtime at a time. I won’t rant about it anymore, at least not today.&lt;/p&gt;
&lt;h2 id=&quot;why-i-blame-hyperscalers&quot;&gt;Why I blame hyperscalers&lt;/h2&gt;
&lt;p&gt;All of the big hyperscalers are based in the US. And because they are, IPv4 addresses are still cheap for them. The rest of the world struggles with the scarcity of the 4 billion addresses, but for &#39;murica, it’s still the default. They don’t have to deploy “solutions” like CGNAT. They can just hand each resident their own stable IPv4 address. And so, they default to their IPv4 only networks. Classical “works for me”, but more like “It works in my country”.&lt;/p&gt;
&lt;p&gt;And because it’s the default for them, the rest of the world has to adapt to that mindset. The rest of the world (including me), have to deal with NAT64, DNS64, 464XLAT and all that other stuff. Just because AWS, Azure and Google are just like: You get an IPv4 and you get an IPv4. Want security? here, have a &lt;code&gt;100.0.13.12/24&lt;/code&gt; and also deploy our NAT gateway. NAT! You want a NAT! You &lt;em&gt;could&lt;/em&gt; enable IPv6, but we make it scary. There are now letters in your IP? 👻
And those IP addresses, they are &lt;em&gt;globally&lt;/em&gt; routable! Globally! Are you sure you want to discuss this with your compliance team? Better stick to NAT and a &lt;code&gt;10.0.0.0/8&lt;/code&gt;, right? Firewalls are for losers.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;To close the circle: Kubernetes defaulting to IPv4 in most of their documentation surely isn’t helping here.
I’ll write a guide for a truly native IPv6 cluster in the future. For now I just had to vent about the situation.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Game review: Split Fiction</title>
    <link href="https://jasminchen.dev/notes/2026/split-fiction/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>d7cf5b64ae382bcacdd06fdfafdd0a4b21b6e8af</id>
    <content type="html">&lt;p&gt;Last weekend, I finished &lt;a href=&quot;https://en.wikipedia.org/wiki/Split_Fiction&quot;&gt;Split Fiction&lt;/a&gt; with my brother. It’s another one of EA’s coop games that have to be played with a friend. I think, &lt;em&gt;A Way Out&lt;/em&gt;, where you have to escape from a prison, is another game of them.&lt;/p&gt;
&lt;p&gt;Aaaaaanyway, back to Split Fiction. The story is relatively simple: You’re playing a writer who wants to publish a new book. The evil capitalist publisher has different ideas and after a short moment, you’re finding yourself in different virtual realities which all were created by either yours or your partners brain. You’re in this together now. That’s the story, more or less. I won’t go into detail here, you can find that on the internet.&lt;/p&gt;
&lt;p&gt;It might sound boring, but it’s absolutely fun. You’re constantly switching between sci-fi and fantasy, you’re fighting evil machines and a cute little kitty. The game has so many wonderful and fun moments, it’s hard to get frustrated. Even in some tougher situations, like boss fights, there are numerous checkpoints that assist the two of you to enjoy the game. It’s challenging enough to not be boring, but also forgiving enough to not be frustrating. I love games like these.&lt;/p&gt;
&lt;p&gt;I also want to appreciate the sheer amount of different environments you’re playing in this game. They are absolutely incredible, crafted with a lot of detail and small easter eggs here and there. It’s colorful, vibrant and dark at the same time. It’s like a cut citrus dipped into sugar: contrasting tastes, yet they fit together perfectly.&lt;/p&gt;
&lt;p&gt;And given it’s co-op and not against each other, me and my brother had a great time playing it.&lt;/p&gt;
&lt;p&gt;There’s probably only one warning I have to give you: The final chapter is longer than we expected. If you’re tired, go to bed and continue it on the next day. Don’t repeat our mistake of hustling through it cause you think you’re close to the end, only to find yourself in a different room.&lt;/p&gt;
&lt;p&gt;Apart from that: 4,5 out of 5 🐾
Absolutely worth every penny.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>About journaling and ADHD</title>
    <link href="https://jasminchen.dev/notes/2026/about-journaling-and-adhd/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>09ba9f6b3b45b77cc4ccbda38068ad5757aca58e</id>
    <content type="html">&lt;p&gt;Since a couple of &lt;s&gt;days&lt;/s&gt; weeks now, I’ve been writing down a daily note every day. This started roughly about the same
time as &lt;a href=&quot;https://jasminchen.dev/notes/2026/first-impressions-of-obsidian/&quot;&gt;I started using Obsidian&lt;/a&gt;. It’s not much, it’s just a simple template that looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;tags:
  - note
  - journal
  - review/daily
created: &quot;&amp;lt;% tp.file.creation_date(&quot;YYYY-MM-DD[T]HH:mm&quot;) %&gt;&quot;
aliases:
- &quot;&amp;lt;% tp.date.now(&quot;dddd MMMM D, YYYY&quot;) %&gt;&quot;
parents:
- &quot;[[&amp;lt;% tp.date.now(&quot;YYYY&quot;) %&gt;]]&quot;
- &quot;[[&amp;lt;% tp.date.now(&quot;YYYY-MM&quot;) %&gt;]]&quot;
- &quot;[[&amp;lt;% moment(tp.file.title, &quot;YYYY-MM-DD&quot;).locale(&quot;de-DE&quot;).format(&quot;YYYY-[W]ww&quot;) %&gt;]]&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&amp;lt;%&#92;*
await tp.file.move(&lt;span class=&quot;token code-snippet code keyword&quot;&gt;`Journal/${tp.file.title}`&lt;/span&gt;);
let titleDate = moment(tp.file.title);

// # Sunday April 5, 2026
tR += &#39;# &#39; + titleDate.format(&#39;dddd, DD MMMM YYYY&#39;);
%&gt;

&lt;span class=&quot;token hr punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &amp;lt;% tp.file.cursor() %&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In case you’re wondering, that’s the syntax for the &lt;a href=&quot;https://silentvoid13.github.io/Templater/&quot;&gt;Templater plugin&lt;/a&gt;, which I adopted recently to play with it. All it does is to generate some metadata and start the template with a h1 of the current day. That’s it. There are a lot of very, I’d say, overengineered templates out there, where they also track their mood, their calories and what not.
I’m not interested in any of that. For me all that matters is that I wrote down what I did at what day. In bullet points, of course.&lt;/p&gt;
&lt;h2 id=&quot;how-it-all-started&quot;&gt;How it all started&lt;/h2&gt;
&lt;p&gt;First, I only used that to be able to properly answer the question &lt;em&gt;What have you done yesterday?&lt;/em&gt; in the daily at work.
In the past, I always tried to remember what I did and that worked &lt;em&gt;most&lt;/em&gt; of the times, but not always. Sometimes, I was just too tired and forgot about things. Writing things down whenver I start working or have worked on a task helped me &lt;strong&gt;a lot&lt;/strong&gt; to answer this question properly. So I continued.&lt;/p&gt;
&lt;p&gt;Each one of us humans probably has another reason for journaling. I, for most of my life, haven’t written a detailed journal. Sure, I used the app “Daylio” a lot in the past but ultimately stopped using that, because it felt like a chore to me. Maybe my current system in Obsidian will turn into the same, I don’t know. Right now, I’m enjoying it a lot.&lt;/p&gt;
&lt;p&gt;And, following the idea of the &lt;a href=&quot;https://github.com/liamcain/obsidian-periodic-notes&quot;&gt;periodic notes plugin&lt;/a&gt;, I started writing a review of my week every Sunday. That’s also the reason why I formatted the date in the &lt;code&gt;de-DE&lt;/code&gt; locale in the template: My week starts at Monday, not at Sunday just like they do it on the other side of the ocean. Anyway. So I started writing a weekly review. It’s a basic Markdown file, named after the year and the week, like &lt;code&gt;2026-W11.md&lt;/code&gt; or so. I review my week by having a look at each individual day again and summarise what I did. Of course, I keep the boring parts out, I just write down what I enjoyed. Whether it was watching a movie in the cinema, a nice compliment I got, stuff like that. I also write down what went wrong and sometimes also things that I want to do next week.&lt;/p&gt;
&lt;h2 id=&quot;the-awakening&quot;&gt;The awakening&lt;/h2&gt;
&lt;p&gt;What I realised after writing the weekly reviews and later, the first monthly review that I did for March, is: &lt;strong&gt;My life is full of things to be excited about!&lt;/strong&gt; Especially in the review I did for March I remembered all the good things that happen. And I also realised: heck, I was doing a lot, no wonder I feel so stressed all the time! The lack of proper time perception, one of the things that this ADHD thingy gave to me, makes me forget certain things and overfocusing on others. I forgot that I was in the cinema in March until I reread my notes again. In my mind, it felt like it was ages ago, when in fact, it was only a mere two weeks in the past. Admittely, I was surprised about the fact of how I easy I can forget things. I mean, it’s one of the most common symptoms of ADHD, it’s just that I hadn’t struggled with it in the past so much. The reason for that, unfortunately, was not that I’m some kind of super brain, no, &lt;strong&gt;I was living with constant anxiety&lt;/strong&gt;. Gonna tell you that, it sucks. It sucks a lot. And it took some years of therapy to get rid of all those fears, of all this inner anxiety, that I &lt;em&gt;might&lt;/em&gt; disappoint someone. And now I’ve got rid of the fears, I just can admit that I forget things. Is this great? nope, not at all. But it’s part of who I am and I’ve come to peace with that.&lt;/p&gt;
&lt;p&gt;If you have ADHD &lt;em&gt;or&lt;/em&gt; you think you might have it, you might see yourself in that paragraph. And everyone of us works different. But, maybe you aren’t writing a journal right now and maybe you think “yeah, this is a new hyperfocus I should jump into!”. And in that case, I can only recommend doing so. And remember, it doesn’t have to be perfect, it doesn’t even have to be complete. It just needs to work for you, &lt;strong&gt;only for you&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The questions I’m asking myself every end of the week now are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What is my highlight of this week?&lt;/li&gt;
&lt;li&gt;What did I enjoy? (books, memes, a silly cat, whatever)&lt;/li&gt;
&lt;li&gt;What did I learn?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I’m regularly surprising myself each week about what I did in the past seven days. Maybe, the same thing will happen for you, dear reader.&lt;/p&gt;
&lt;p&gt;cheers.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Impressions of the the KubeCon 2026 Europe</title>
    <link href="https://jasminchen.dev/notes/2026/kubecon-2026/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>1214dc3d45ab11686954276816cee6c2164bcae5</id>
    <content type="html">&lt;p&gt;Last week, I attended my first &lt;a href=&quot;https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/&quot;&gt;KubeCon&lt;/a&gt;, or as it’s officially named, the &lt;em&gt;KubeCon + CloudNativeCon Europe 2026&lt;/em&gt; in Amsterdam. In fact, this was my first conference in the tech space that’s not affiliated to the Chaos Computer Club in any way.&lt;/p&gt;
&lt;p&gt;I wanted to visit Amsterdam for a while now, because their &lt;a href=&quot;https://www.youtube.com/watch?v=aESqrP3hfi8&quot;&gt;bicycle infrastructure is known to be ahead of everyone else&lt;/a&gt;. I wanted to experience this on my own. Originally, my plan was to ride a bicycle on my own while being there, but due to the KubeCon being tightly packed and full of interesting discussions with my colleagues, I didn’t had the chance to do it. Maybe another day. ^^&lt;/p&gt;
&lt;h2 id=&quot;my-impressions-of-the-conference&quot;&gt;My impressions of the conference&lt;/h2&gt;
&lt;p&gt;Anyway, back to the KubeCon. To summarise it: It was awesome. However, that’s not really related to the event itself, but rather the occassional chit-chat with colleagues and strangers alike. It’s somehow refreshing to see a lot of
humans, all trying to have a nice time. Of course, with the KubeCon being a really expensive and corporate event, there were a lot of sponsors there, all of them trying to sell you their stuff. And, because it’s 2026, the sheer amount of them advertised something related to &lt;a href=&quot;https://jasminchen.dev/notes/2026/fuck-ai&quot;&gt;“AI”&lt;/a&gt;. I wonder if they realised how silly their stuff sounded like. Seriously, why does someone want to interact with their infrastructure using a chatbot? I don’t get it. But I guess, I’m not the target audience, so I ignored all talks and booths which mentioned “AI”.&lt;/p&gt;
&lt;p&gt;The remaining talks I attended felt somewhat &lt;em&gt;flat&lt;/em&gt;, none of those sparked the feeling of experimenting with the tech itself. They were all pretty boring.&lt;/p&gt;
&lt;p&gt;Also, the food: Apparently, they used pre-packaged sandwiches last year. This year, we got better options: warm meals, with a lot of variation to choose from. There’s just one complaint: The portions were rather small. I guess the benefit of that is that visitors do not get tired because they ate too much. 😬&lt;/p&gt;
&lt;p&gt;Discussing things, whether work-related or not, with my colleagues and everyone I met there, was the best thing of the conference. I learned a lot about the company-internal lore, which helped me tremendously understanding why things are the way they are. I also learned things about the &lt;a href=&quot;https://cluster-api.sigs.k8s.io/&quot;&gt;cluster-api&lt;/a&gt;, which I really want to try out soon.&lt;/p&gt;
&lt;h2 id=&quot;impressions-of-amsterdam&quot;&gt;Impressions of Amsterdam&lt;/h2&gt;
&lt;p&gt;The city is &lt;strong&gt;so. damn. beautiful!&lt;/strong&gt;. Most of the times when I was outside, the sun was shining. There are bikes everywhere. And I can understand why. The bicycle infrastructure is really something else. At no point I felt unsafe, either as a pedestrian and I can imagine that cyclists feel the same. The paths are wide, with lots of room for anyone to travel safe. The narrow alleys, the wide streets, the amount of pedestrians — it won’t be my last time attending this place. Next time I’ll be here, I wanna ride a bicycle and drink a fresh cup of coffee on one of the boats.&lt;/p&gt;
&lt;p&gt;With that being said, here’s a photo I took while being there:&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-1000.avif 1000w, https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-2000.avif 2000w, https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-3024.avif 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-1000.webp 1000w, https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-2000.webp 2000w, https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-3024.webp 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-1000.jpeg&quot; alt=&quot;A photo down a narrow alley in Amsterdam. No people are present. The residents put some greenery outside, like smaller plants and trees. On the right side, there&#39;s a small tree leaning into the middle of the alley, providing some shadow. All of the houses look rather old, yet, they&#39;re very clean, just like the alley itself. The whole picture invites the viewer to imagine some kobolds or elves hiding somewhere behind a bicycle.&quot; width=&quot;3024&quot; height=&quot;4032&quot; srcset=&quot;https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-1000.jpeg 1000w, https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-2000.jpeg 2000w, https://jasminchen.dev/notes/2026/kubecon-2026/UGrJkD519F-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Project Hail Mary</title>
    <link href="https://jasminchen.dev/notes/2026/project-hail-mary/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>53ee190ebbc06b5e56c574ed48293acca3a41701</id>
    <content type="html">&lt;p&gt;Last Sunday, I watched &lt;a href=&quot;https://www.imdb.com/title/tt12042730/&quot;&gt;Project Hail Mary&lt;/a&gt; in the local cinema. I’ve read the book before, which I enjoyed as well! At first, I was a bit sceptical about the movie. I really hoped that they didn’t turn it into a &lt;em&gt;Marvel Cinematic Universe (MCU)&lt;/em&gt;-like movie, full of self-ironic comments, action scenes with a lot of cuts, you know, just another big Hollywood movie. The book was surprisingly calm and it could transport the fears that Ryland Grace, the protagonist, must have felt. The sheer emptiness of space.&lt;/p&gt;
&lt;p&gt;I’m glad to see that they succeeded with the movie. I re-experienced the same feelings I had when reading the book. The fascination, the sad and the cheering moments, they’re all in the movie. The script is &lt;strong&gt;really, &lt;em&gt;really&lt;/em&gt;&lt;/strong&gt; close to the book.&lt;/p&gt;
&lt;p&gt;The only thing that was a bit frustrating, is that they squeezed the last 15 minutes or so. There’s some story that could’ve been told, but was likely cut to not bore the audience. I really hope that there will be an extended cut in the future, even just half an hour could do a lot. But that’s the only thing I have to complain about. Besides that: 5/5 stars, I can only recommend it!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>now with human.json!</title>
    <link href="https://jasminchen.dev/notes/2026/now-with-human-json/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>9956a246a14cfae0930fbd578b70c16fa8b26b3c</id>
    <content type="html">&lt;p&gt;Originally found over at the blog of &lt;a href=&quot;https://sethmlarson.dev/ive-added-human-dot-json-to-my-website&quot;&gt;Seth Larson&lt;/a&gt;, the &lt;a href=&quot;https://codeberg.org/robida/human.json&quot;&gt;human.json&lt;/a&gt; thingy is a nice concept I adore. You just add a lil &lt;code&gt;human.json&lt;/code&gt; to your webpage, &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; to it and 🎉, you are hooman now!&lt;/p&gt;
&lt;p&gt;And now I have one too! Right now, without any vouchers. Have to think about what it means to &lt;em&gt;vouch&lt;/em&gt; for someone. Until then, I keep it empty.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Building a pollen overview in HomeAssistant</title>
    <link href="https://jasminchen.dev/notes/2026/building-a-pollen-overview-in-home-assistant/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>ffe06dadceba426c0e40faa58ff84c339cf4ee87</id>
    <content type="html">&lt;p&gt;In this flat, we have an old Android tablet serving as the dashboard. Given it’s spring again, nature is getting horny again and as a result, there are more pollen outside. For someone with an allergy (hi, it’s me), this is pretty annoying.&lt;/p&gt;
&lt;p&gt;Therefore, I wanted to have a list of pollen and their current prevalence in the air, so that I can my assess my need for antiallergens.&lt;/p&gt;
&lt;p&gt;To do this, I ended up with the following Jinja2 template inside a &lt;a href=&quot;https://www.home-assistant.io/dashboards/markdown/&quot;&gt;Markdown card&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-jinja2&quot;&gt;&lt;code class=&quot;language-jinja2&quot;&gt;&lt;span class=&quot;token jinja2 language-jinja2&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;token tag keyword&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;pollen&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;integration_entities&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;dwd_pollenflug&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;attribute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;state&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;list&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;%}&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token jinja2 language-jinja2&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{%-&lt;/span&gt; &lt;span class=&quot;token tag keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;pollen&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;%}&lt;/span&gt;&lt;/span&gt;
- &lt;span class=&quot;token jinja2 language-jinja2&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;: &lt;span class=&quot;token jinja2 language-jinja2&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;state_attr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;entity_id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;state_today_desc&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token jinja2 language-jinja2&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{%-&lt;/span&gt; &lt;span class=&quot;token tag keyword&quot;&gt;endfor&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;%}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is using the &lt;a href=&quot;https://github.com/mampfes/hacs_dwd_pollenflug&quot;&gt;DWD Pollenflug integration&lt;/a&gt;, which I installed via HACS previously. Given that I prefer text much over images and colors to indicate something, I chose the Markdown card. The integration also provides some visual examples, if you’re more into that.&lt;/p&gt;
&lt;p&gt;The list is sorted, which, after renaming the entities, ended up in the following result:&lt;/p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://jasminchen.dev/notes/2026/building-a-pollen-overview-in-home-assistant/rMQ3KPUWfL-396.avif 396w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://jasminchen.dev/notes/2026/building-a-pollen-overview-in-home-assistant/rMQ3KPUWfL-396.webp 396w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://jasminchen.dev/notes/2026/building-a-pollen-overview-in-home-assistant/rMQ3KPUWfL-396.png&quot; alt=&quot;A screenshot of the rendered template within HomeAssistant. It got extended to also display the state for tomorrow and presents the current pollen sources: alder, ash and hazel.&quot; width=&quot;396&quot; height=&quot;312&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>First impressions of Obsidian</title>
    <link href="https://jasminchen.dev/notes/2026/first-impressions-of-obsidian/" />
    <updated>2026-06-01T21:28:32Z</updated>
    <id>f5f924d35961f91adebf1f6ede7042e3ad018597</id>
    <content type="html">&lt;p&gt;Two days ago, I noticed that I mounted the wrong folder for my &lt;a href=&quot;https://getoutline.app&quot;&gt;Outline&lt;/a&gt; database deployment. I originally set it up because I wanted something to document this flat, similar to what &lt;a href=&quot;https://luke.hsiao.dev/blog/housing-documentation/&quot;&gt;Luke Hsiao described in his blog&lt;/a&gt;. It was a pretty minor mistake. Instead of mounting &lt;code&gt;/var/lib/postgresql&lt;/code&gt; I mounted &lt;code&gt;/var/lib/postgres&lt;/code&gt;, resulting in a loss of the whole database.&lt;/p&gt;
&lt;h2 id=&quot;synctrain-obsidian-works-pretty-nice-even-on-an-iphone&quot;&gt;Synctrain + Obsidian works pretty nice, even on an iPhone&lt;/h2&gt;
&lt;p&gt;I haven’t setup backups either, as this was a thing I had on my bucket list for this weekend. Honestly, this was and still is pretty frustrating. I wrote a lot of documentation and now it’s lost.
As a result, I was experimenting with other tools again, notably &lt;a href=&quot;https://obsidian.md/&quot;&gt;Obsidian&lt;/a&gt;. Although it’s proprietary, it has a massive user base. And, together with &lt;a href=&quot;https://apps.apple.com/gb/app/synctrain/id6553985316&quot;&gt;Synctrain&lt;/a&gt;, an iOS app for Syncthing, it allows me to sync notes from my iPhone to the desktop as well. The mobile editing experience of Outline was subpar, so that actually was a pretty nice upgrade.&lt;/p&gt;
&lt;h2 id=&quot;importing-existing-notes&quot;&gt;Importing existing notes&lt;/h2&gt;
&lt;p&gt;After setting up the sync and making sure it’s properly backed up this time on the remote host, I started to copy some notes I had locally on my iPhone in Apple Notes over to Obsidian. There’s an importer for that as well, but it only works on macOS and I have such no device at hand, so manual copy &amp;amp; paste was the way to go.
After that was done, I had a quick glance at my &lt;code&gt;~/Documents&lt;/code&gt; folder and copied existing plain text notes into the Obsidian folder as well. Not because it was necessary, but rather out of excitement I think. I have this new note organisation tool, it only makes sense if I copy all my existing notes into it.&lt;/p&gt;
&lt;h2 id=&quot;on-obsidian-plugins&quot;&gt;On Obsidian plugins&lt;/h2&gt;
&lt;p&gt;There’s a pretty huge ecosystem around Obsidian and a lot of plugins people have written for it. So far, I haven’t used any of them. I’m “limited” to the built-in core plugins right now and even disabled a good amount of them, like the Canvas plugin. I don’t have a need to use that right now and since it somehow clutters the view, I disabled it.&lt;/p&gt;
&lt;h2 id=&quot;things-i-like&quot;&gt;Things I like&lt;/h2&gt;
&lt;p&gt;The plugin system is probably one of the core strengths of Obsidian. I like what they’re doing with e.g. the “Daily note” plugin, which allows me to write a small note every day. &lt;a href=&quot;https://www.ntietz.com/blog/using-an-engineering-notebook/&quot;&gt;Nicole recommended to write an engineering notebook&lt;/a&gt;, which is a thing I haven’t done so far. Dealing with a lot of simultaneously things at work really made me consider writing one, however. I think I’ll stick to a digital form for my first experiments, might switch to a paper-based one later.
I also like the editor experience and the ability to customize all the shortcuts. I use the keyboard pretty extensively and therefore it’s always nice to have everything at your hands.&lt;/p&gt;
&lt;h2 id=&quot;things-i-dislike&quot;&gt;Things I dislike&lt;/h2&gt;
&lt;p&gt;Notes are missing some metadata by default, like the creation date. I wish that those would be tracked automatically, e.g. from the file metadata. So far, I haven’t found a way to display that. On the one hand, that’s good, because documentation and notes are things that evolve all the time. On the other hand, it’s metadata that I’d like to see sometimes to actually know when I wrote something down. For now, I backfilled the date manually. I have to figure out whether that’s something that’ll block me in the future from using Obsidian.&lt;/p&gt;
&lt;h2 id=&quot;next-up&quot;&gt;Next up&lt;/h2&gt;
&lt;p&gt;So far, that’s it. This blog post is also written in Obsidian and I’m wondering right now, whether I want – and to which extent – hook this up to my 11ty-based setup. Writing blog posts in Obsidian sounds like a thing I want to consider. Maybe I’ll begin to use Obsidian as some kind of CMS for my homepage, but I’m not so sure about that yet. Maybe I’ll also ditch it completely and replace it with a DokuWiki-like solution.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Experimenting a bit with coding fonts</title>
    <link href="https://jasminchen.dev/notes/2026/experimenting-a-bit-with-coding-fonts/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>13824cc94b8effa12b44d3bebbbc39c3ea867a68</id>
    <content type="html">&lt;p&gt;Recently, I played a bit more with the fonts I use for my programming tasks.
There’s not a strong reason to do so, I just like to change the appeareance of my code from time to time. Also, using patterns change and that also causes new fonts to be explored. For example, I used &lt;em&gt;Iosevka&lt;/em&gt; especially in a time where I was reading logs a lot of the day and the way it’s condensed made it really good for this task!&lt;/p&gt;
&lt;p&gt;When having a look at the history of the fonts, I used, there are actually &lt;strong&gt;a lot&lt;/strong&gt;. Like, I used &lt;a href=&quot;https://github.com/tonsky/FiraCode&quot;&gt;Fira Code&lt;/a&gt; for a long time. Then, after a while, I switched to Microsoft’s new font, &lt;a href=&quot;https://github.com/microsoft/cascadia-code&quot;&gt;Cascadia Code&lt;/a&gt; immediately when it came out. For a while, I even used the much “beloved” &lt;em&gt;Courier New&lt;/em&gt;, just as an experiment.&lt;/p&gt;
&lt;p&gt;The font I used for the last year was &lt;a href=&quot;https://github.com/mishamyrt/Lilex&quot;&gt;Lilex&lt;/a&gt;, which is based on &lt;em&gt;IBM Plex Mono&lt;/em&gt;. I guess that’s the beauty of open source licenses, you can just take something you like and adapt it even more to your own taste. And it’s a good font! I really liked it!&lt;/p&gt;
&lt;p&gt;Today, however, I had the inner desire to switch to a more &lt;em&gt;cozy&lt;/em&gt; font. A bit more rounded, a bit softer, yeah, I wanted the vibe of the font to be like a sunny day in spring. Imagine drinking a hot cup of coffee (or tea) in a small café in April. The sun is shining, the sky is blue and from time to time, there’s a breeze of wind going through your hair. That’s what I want my font to be right now.&lt;/p&gt;
&lt;p&gt;And that’s why I’m choosing &lt;a href=&quot;https://font.subf.dev/en/&quot;&gt;Maple Mono&lt;/a&gt; for the next adventure. I installed it today on my work device and now, when I’m writing this blog post, I’m also using it on my private device as well. And I really love the effort that went into this. It doesn’t feel too rounded (which is a problem I had with &lt;em&gt;Comic Mono&lt;/em&gt;). It’s also not too wide, which I worried about initial. It just &lt;em&gt;feels right&lt;/em&gt;. I really appreciate the effort that went into this.&lt;/p&gt;
&lt;p&gt;Now, let’s see how long I’ll stick to this font. I just changed it to be the default font for this blog as well and I really like how it turned out!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Turns out, resident IPv6 addresses appear to be stable</title>
    <link href="https://jasminchen.dev/notes/2026/ipv6-stable-addresses/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>5de7db5d6df67492d7f8f15bcadc00f51e029edf</id>
    <content type="html">&lt;p&gt;Recently, I migrated the &amp;lt;queer.group&amp;gt; Mastodon server to a new server. Primary reason for that was a lot of cruft that was ín the old Debian installation. Another reason to switch was to migrate to a distro that has most, if not all of the dependencies in its package repositories. Long story short, the distro I chose is Arch Linux, which even has a &lt;a href=&quot;https://aur.archlinux.org/packages/mastodon&quot;&gt;mastodon package in the Arch User Repository&lt;/a&gt;, which I was able to use as reference.&lt;/p&gt;
&lt;p&gt;Because I just wanted to experiment a bit before exposing the server to the public, I was setting up a firewall upfront. Luckily, Hetzner makes this quite easy and because the server was IPv6-only at the time of setup, I just copied my public IPv6 address into the allowed incoming IPs and disallowed everything else.&lt;/p&gt;
&lt;p&gt;I expected this to break after a couple of hours. It’s quite common here in Germany for IPv4 addresses to be rotated daily, so I expected that my IPv6 address would be changing as well. Well, maybe not quite as often, but I at least expected the last 64 bits to be changed on reboots and such. &lt;strong&gt;Turns out, they don’t.&lt;/strong&gt; I was quite surprised by this! After &lt;a href=&quot;https://queer.group/@jasmin/115995412599926846&quot;&gt;mentioning said observation in the fediverse&lt;/a&gt;, I got a reply that provides more information about the stability of the IPv6 address, mentioning three changes within 10 years. That, of course, only applies if you’re sticking with the same ISP for the whole duration and do not replace your device.&lt;/p&gt;
&lt;p&gt;I have to admit, I’m quite pleased by this. I have to conduct some more experiments, but if it remains stable, it means I might just point hardcode individual IPv6 addresses in, e.g. my Prometheus configuration instead of sticking to a solution like Tailscale.&lt;/p&gt;
&lt;p&gt;IPv6-only servers have another net benefit. In my experience, it’s sufficient enough to keep most of the malicious scrapers out.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>I am so sick of the slop.</title>
    <link href="https://jasminchen.dev/notes/2026/fuck-ai/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>2095f68528039d5a13e15040afb14e2a9098fb12</id>
    <content type="html">&lt;p&gt;I am so tired of this bubble. It’s been, ugh, seven years or so. Yes, the “AI” bubble.
I am so tired of the endless justifications, the endless repetitions of “in six months, we will see [insert unrealistic expectation here]”.&lt;/p&gt;
&lt;p&gt;And I am one of the lucky ones. I do not have to work with it. No manager is forcing this bullshit down my throat. At least, they don’t threaten me with it.&lt;/p&gt;
&lt;p&gt;But I swear to god, the two occurrences of “Yesterday, I chatted a bit with Gemini and this is what it came up with, what do you think?” were more than enough. In both cases the output was mediocre at best. Even though I try to be polite, sometimes it’s &lt;em&gt;hard&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;two-examples-where-it-got-worse&quot;&gt;Two examples where it got worse&lt;/h2&gt;
&lt;p&gt;And even if I don’t interact with one of the slop machines directly, I do notice the degradation in the software I use (or better: have to use) every day. The Bitwarden client extension randomly answers with “wrong master password”, because it’s trying to decode the wrong vault. They have a multi-user functionality, apparently no one is testing it. And it worked in the past.&lt;/p&gt;
&lt;p&gt;Speaking of developer toolings: GitHub got massively worse. Just yesterday, the scrolling position was randomly reset when I was looking at a bigger patch on my phone. It used to be a good website, from a technical perspetive. But that used to be.&lt;/p&gt;
&lt;h2 id=&quot;consequence-lack-of-trust&quot;&gt;Consequence: Lack of trust&lt;/h2&gt;
&lt;p&gt;I could open a bug report, but honestly, I don’t want to anymore. Either it’s one of the issues that’s closed by fucking &lt;code&gt;stalebot&lt;/code&gt; again or the half-baked solution doesn’t fix it either. I lost my trust in such processes and only do I have some trust into the maintainers to actually have a look at the problem. That’s what this bubble did to me. I hate talking to a chatbot, I also don’t want that some slop machine just looks at my issue and interprets it in its own ways.&lt;/p&gt;
&lt;h2 id=&quot;you-must-be-kidding-me-right&quot;&gt;You must be kidding me, right?&lt;/h2&gt;
&lt;p&gt;Slop at work is one thing. But slop by credible artists, that’s a whole different thing. I just wanted to watch your concert, Jean-Michel. But you said something along the lines of “We shouldn’t be afraid of technology” and blabber something about how great it is for artists to express themselves (&lt;a href=&quot;https://www.arte.tv/en/videos/130043-000-A/jean-michel-jarre/&quot;&gt;Minute 55:50 ff. of this concert on ARTE&lt;/a&gt;). Go fuck yourself. Seriously, I hope you realise about the damage you’ve done with that.&lt;/p&gt;
&lt;h2 id=&quot;further-notes&quot;&gt;Further notes&lt;/h2&gt;
&lt;p&gt;If you understand German, there’s a good &lt;a href=&quot;https://www.youtube.com/watch?v=kUuwvJfQfIk&quot;&gt;video by Ultralativ&lt;/a&gt; about the feeling of betrayal if you receive slop and how the sender more or less told you that they do not respect you at all as a person.&lt;/p&gt;
&lt;p&gt;Did I mention that I’m tired? Cause I am. But writing this all down helped me to let the frustration go away. At this point, I just hope that the bubble bursts rather sooner than later.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Using OAuth as a Git credential helper</title>
    <link href="https://jasminchen.dev/notes/2025/using-o-auth-as-a-git-credential-helper/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>60e40a51f22e449ddc7da5da4e6ac716365e5221</id>
    <content type="html">&lt;p&gt;When I was hacking on &lt;a href=&quot;https://forgejo.org&quot;&gt;Forgejo&lt;/a&gt; in the hospital recently, I was blocked by
their firewall which blocked outgoing access via SSH. Sure, I could’ve started my VPN and try to
circumvent their silly policies, but that’s not the first thing that came to my mind. The first
thing, in fact, was to use the &lt;code&gt;https&lt;/code&gt; URLs for pushing to Git.&lt;/p&gt;
&lt;p&gt;Usually when using HTTP endpoints, you’re either using your username/password combination, or like
in my case with two-factor authentication being enabled, a &lt;em&gt;personal access token&lt;/em&gt;. So, I went to
the “Applications” settings on Codeberg and… found old access tokens. Never used, but they’ve been
lurking there for two years now.&lt;/p&gt;
&lt;p&gt;And I hate that. I hate that there are theoretically tokens that I just forgot about. The idea of
short-lived credentials, as in “tokens that expire after a couple of hours” is, at least in my
opinion, better suited for such things.&lt;/p&gt;
&lt;p&gt;Luckily, with the help of the
&lt;a href=&quot;https://github.com/hickford/git-credential-oauth&quot;&gt;hickford/git-credential-oauth project on GitHub&lt;/a&gt;,
pushing to a remote repository now just opens the browser. Most of the time I’m already
authenticated there, so the operation almost instantly succeeds. I really like this approach! And
although I think that SSH is still beneficial in most cases, I’m thinking about dropping it entirely
for most repositories and stick to HTTPS + OAuth instead. After all, how often do you rotate your
SSH keys? If you’re like me, maybe once every five years or so. 🙈&lt;/p&gt;
&lt;p&gt;One little adjustment in the Git configuration later, and I’m good to go. Thanks a lot for this
tooling, &lt;em&gt;M Hickford&lt;/em&gt; ❤️&lt;/p&gt;
&lt;pre class=&quot;language-conf&quot;&gt;&lt;code class=&quot;language-conf&quot;&gt;[credential]
	helper = cache --timeout 43200	# 12 hours
	helper = oauth&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Also, while thinking about it, I should separate the SSH keys I use for signing and
authentication.)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>IPv6 is still a pain in the ass</title>
    <link href="https://jasminchen.dev/notes/2025/ipv6-is-still-a-pain-in-the-ass/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>1d50f4b83815762d6f4fb61b3e8b294e38a06eeb</id>
    <content type="html">&lt;p&gt;When I was setting up a new server in an IPv6-only fashion, I was like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Yeah, I know that there are certain things that won’t work out of the box &lt;em&gt;cough&lt;/em&gt; GitHub &lt;em&gt;cough&lt;/em&gt;,
but overall, it should be fine, right? Right?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And oh boy, I was not prepared for that journey.&lt;/p&gt;
&lt;p&gt;Let’s start with the environment. It’s 2025, and I am playing around with Fedora 43 (aarch64) on a
Hetzner VM. So far, so basic. The operating system is not an outlier, the cloud provider is known
and ARM architectures are so common nowadays that it’s only a mere sidefact.&lt;/p&gt;
&lt;p&gt;In fact, when setting up the system, it was much more likely that I found aarch64 binaries on
IPv4-only hosts than only x86 binaries on dualstack/IPv6-only hosts. &lt;strong&gt;And that, excuse me, fucking
sucks.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;IPv6, &lt;em&gt;checks notes&lt;/em&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/IPv6&quot;&gt;gets 30 years old in two months&lt;/a&gt;. Thirty
years. And yet, it’s treated as some outlier, because some &lt;s&gt;dickheads&lt;/s&gt; nice folks at big cloud
providers are like: oh nu, please no hexadecimal numbers 🥺 only AI! buy more AI, so we can fire you
🥺🥺&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Himmelherrgottnochmal&lt;/em&gt;, it’s not that hard. Come on, use that “agent” and let it explain IPv6 to
you. Vibe engineer your network infrastructure if you want to, but please, implement IPv6 now.&lt;/p&gt;
&lt;p&gt;(jasmin, come on, calm down.)&lt;/p&gt;
&lt;p&gt;So, let me tell you a story. To manage the installation of several tools in use, I am using
&lt;a href=&quot;https://mise.jdx.dev/&quot;&gt;mise&lt;/a&gt; at the moment. And it has a nice feature: If &lt;code&gt;cosign&lt;/code&gt; or
&lt;code&gt;slsa-verifier&lt;/code&gt; are installed, it can check the downloaded binaries against that (if they use either
one of them). Which works pretty nice, unless, you’ve guessed it, you don’t have an IPv4 address.&lt;/p&gt;
&lt;p&gt;That’s right, &lt;em&gt;sigstore.dev&lt;/em&gt;, the thing that should help with security, the thing that claims
“Making sure your software is what it claims to be.”, the thing that is supported by no one less
than GitHub, Hewlett Packard, Cisco, Google and Red Hat, yea, &lt;strong&gt;that thing does not support IPv6&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;dig&lt;/span&gt; AAAA sigstore.dev
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; DiG &lt;span class=&quot;token number&quot;&gt;9.18&lt;/span&gt;.41 &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; AAAA sigstore.dev
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; global options: +cmd
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; Got answer:
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; -&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;HEADER&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;-&lt;/span&gt; opcode: QUERY, status: NOERROR, id: &lt;span class=&quot;token number&quot;&gt;4442&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; flags: qr rd ra&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; QUERY: &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;, ANSWER: &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;, AUTHORITY: &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;, ADDITIONAL: &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; OPT PSEUDOSECTION:
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; EDNS: version: &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;, flags:&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; udp: &lt;span class=&quot;token number&quot;&gt;65494&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; QUESTION SECTION:
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;sigstore.dev.			IN	AAAA

&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; AUTHORITY SECTION:
sigstore.dev.		&lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;	IN	SOA	ns-cloud-a1.googledomains.com. cloud-dns-hostmaster.google.com. &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;21600&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3600&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;259200&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; Query time: &lt;span class=&quot;token number&quot;&gt;69&lt;/span&gt; msec
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; SERVER: &lt;span class=&quot;token number&quot;&gt;127.0&lt;/span&gt;.0.53&lt;span class=&quot;token comment&quot;&gt;#53(127.0.0.53) (UDP)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; WHEN: Sun Nov 02 00:12:45 CET &lt;span class=&quot;token number&quot;&gt;2025&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; MSG SIZE  rcvd: &lt;span class=&quot;token number&quot;&gt;134&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a joke, right? This must be. I mean, come on. If you are launching a new product (and it
says Copyright 2023 in the footer) and you’re not incorporating IPv6 right from the beginning, are
you even trying? I mean, what is this? Are citizens of the so called United States of America all
gifted a whole IPv4 subnet on their birth or why are they like this?&lt;/p&gt;
&lt;p&gt;I am not angry. I am just frustrated. I don’t understand it. I wish I could, but… I can’t. Yes,
IPv6 is different from IPv4. There’s a lot of stuff that’s different. There’s a lot of stuff that’s
not possible with IPv4, like
&lt;a href=&quot;https://en.wikipedia.org/wiki/IPv6_address#Stateless_address_autoconfiguration_(SLAAC)&quot;&gt;SLAAC&lt;/a&gt;
and that might be scary first. But please, for the love of your business partners and users alike,
&lt;strong&gt;please implement/rollout IPv6 now, if you haven’t done it yet.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Thank you.&lt;/p&gt;
&lt;h2 id=&quot;update-2025-11-02-reply-from-the-sigstore-dev-team&quot;&gt;Update 2025-11-02: Reply from the sigstore.dev team&lt;/h2&gt;
&lt;p&gt;Complaining in the internet about stuff is one thing, but it doesn’t help. Which is why I reached
out to the sigstore folks via their mailing list to understand what’s going on there. The reply left
me even more speechless.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our main website host (used for our homepage and docs) unfortunately does not support IPv6 unless
we use their DNS provider, which we’ve chosen not to do due to a lack of functionality. As far as
our various endpoints used as part of the signing or verification process - it’s on our roadmap to
support IPv6, but we don’t have a definitive date set yet.
&lt;cite&gt;&lt;a href=&quot;https://groups.google.com/g/sigstore-dev/c/ZlLGYdmsggo/m/hN8hUkNfAwAJ?pli=1&quot;&gt;Bob
Callaway (via Google Groups)&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;According to this
&lt;a href=&quot;https://hosting-checker.net/websites/sigstore.dev&quot;&gt;random hosting checker website I found&lt;/a&gt;, the
page is hosted on AWS. It’s wild that they are trying to make a business out of IPv6. capitalism is
wild.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>This homepage is now IPv6-only</title>
    <link href="https://jasminchen.dev/notes/2025/ipv6-only/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>62e124c6df579c2f336edfdf1b0a04643c9bc7f7</id>
    <content type="html">&lt;p&gt;If you can read this in your feed reader, congratulations.&lt;/p&gt;
&lt;h2 id=&quot;why&quot;&gt;Why?&lt;/h2&gt;
&lt;p&gt;I migrated this homepage to another server. And because I don’t think that it’s worth paying for
IPv4, I decided to just get rid of it. After all, it’s 2025, literally &lt;strong&gt;anything&lt;/strong&gt; should(!)
support IPv6 now.&lt;/p&gt;
&lt;p&gt;And if it doesn’t, it’s broken.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Calibrating a PS5 controller on Linux</title>
    <link href="https://jasminchen.dev/notes/2025/fixing-the-stick-drift-of-a-ps5-controller-under-linux/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>df653d125d3b3886b5ab98fd3f4aec88d8b111ae</id>
    <content type="html">&lt;p&gt;Recently, I bought a used PS5 to play some video games. Because it was used and not new, the
controller had this pretty common problem called “stick drift”. While there are possibilities to fix
this in the long term (thanks to all the awesome ifixit folks who write guides for this!), I wanted
to know whether there’s a short-term solution.&lt;/p&gt;
&lt;p&gt;And to my surprise, there is! Someone built a
&lt;a href=&quot;https://dualshock-tools.github.io/&quot;&gt;DualShock Calibration GUI&lt;/a&gt;, which uses WebUSB to recalibrate
the controller. It was worth a try. So, after searching for a proper USB cable, I plugged it into
the computer, opened the page in a Chromium browser (because Firefox doesn’t support the WebUSB
APIs) and:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Failed to open the device.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;adding-missing-permissions-via-udev&quot;&gt;Adding missing permissions via udev&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;sighs&lt;/em&gt; I love it when things would just work. But eh, because my keyboard, the ZSA Moonlander,
needed some adjustments as well, so that I can use their online flashing tool, I already knew where
to look! The software I had to configure is &lt;a href=&quot;https://wiki.archlinux.org/title/Udev&quot;&gt;udev&lt;/a&gt;. When your
system also uses &lt;em&gt;systemd&lt;/em&gt; (and almost all of them do that for a good reason), this likely applies
to you as well.&lt;/p&gt;
&lt;p&gt;udev itself is configured using &lt;em&gt;rules&lt;/em&gt;. Those rules can do a lot of stuff, but in this case, all I
needed to do was to change the permissions. By default, devices are only accessible to the root user
and I certainly don’t wanna run my browser as root, hell no.&lt;/p&gt;
&lt;p&gt;There’s a lot one can do with udev and to be honest, I haven’t looked into all the things that are
doable with that. All I wanted was to recalibrate my controller. Similar to the udev rule for my
Moonlander, I tried to create a new one for my controller in
&lt;code&gt;/etc/udev/rules.d/60-playstation.rules&lt;/code&gt;. This was the one I ended up with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Vendor 054c is Sony. This rule is for the PlayStation controllers.
KERNEL==&amp;quot;hidraw*&amp;quot;, ATTRS{idVendor}==&amp;quot;054c&amp;quot;, MODE=&amp;quot;0660&amp;quot;, TAG+=&amp;quot;uaccess&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Update 2025-10-29&lt;/strong&gt;: After I read more abot udev, I stumbled upon the following information in the
&lt;a href=&quot;https://wiki.archlinux.org/index.php?title=Udev&amp;amp;oldid=848650#Allowing_regular_users_to_use_devices&quot;&gt;Arch Wiki&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The OWNER, GROUP, and MODE udev values can be used to provide access, though one encounters the
issue of how to make a device usable to all users without an overly permissive mode. Ubuntu’s
approach is to create a plugdev group that devices are added to, but this practice is not only
&lt;strong&gt;discouraged by the systemd developers&lt;/strong&gt;, [2] but considered a bug when shipped in udev rules on
Arch (FS#35602).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The above udev rule was therefore adjusted to reflect this change.&lt;/p&gt;
&lt;h3 id=&quot;sidenote-how-to-figure-out-the-usb-vendor&quot;&gt;Sidenote: How to figure out the USB vendor&lt;/h3&gt;
&lt;p&gt;I figured out the vendor ID via &lt;code&gt;lsusb&lt;/code&gt;. &lt;code&gt;lsusb&lt;/code&gt; outputs a list like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first part of the ID is the vendor. In case of the USB hub above, it’s &lt;code&gt;1d6b&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;
&lt;p&gt;After saving the rule above, I rebooted the computer. If I remember this right, this &lt;strong&gt;must&lt;/strong&gt; be
done in order for the rules to take effect. The above rule adds write permissions to &lt;s&gt;the &lt;code&gt;plugdev&lt;/code&gt;
group&lt;/s&gt; currently logged in users. &lt;s&gt;My user was already part of it, as I’ve created the &lt;code&gt;plugdev&lt;/code&gt;
group earlier.&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;Anyway, after the reboot, I tried my luck again and: &lt;strong&gt;it worked!&lt;/strong&gt; I was able to recalibrate my
controller! With the recalibration in place, I was able to continue playing
&lt;a href=&quot;https://humanity.game/&quot;&gt;Humanity&lt;/a&gt; (can fully recommend it!!) without getting annoyed by the drift.
Nonetheless, I hope that Sony releases a new version of the PS5 controller with a replaceable
battery soon, which would allow me to buy a new one and mod it with &lt;em&gt;tunnel magnetoresistance&lt;/em&gt;
joysticks, like
&lt;a href=&quot;https://www.ifixit.com/en-eu/products/ps5-dualsense-controller-gulikit-tmr-joystick&quot;&gt;the Guilkit joysticks in the iFixit store&lt;/a&gt;.
But that’s another thing to look at in the future.&lt;/p&gt;
&lt;h3 id=&quot;update-2025-10-23&quot;&gt;Update 2025-10-23&lt;/h3&gt;
&lt;p&gt;Because the stick drift is a mechanical problem, the above solution is only a partial fix.
Sometimes, after rebooting the controller, the drift appears again. I find that rather annoying,
even though I expected it. welp. Guess, my plan to replace the joysticks has to happen soon.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>System administration contradicts perfectionism</title>
    <link href="https://jasminchen.dev/notes/2025/system-administration-contradicts-perfectionism/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>62aeb0aca7b9556cc7117f0c6305a51e82aa8e75</id>
    <content type="html">&lt;p&gt;Or perfectionism contradicts system administration. I am not sure. However, after my last
experiments with system administration (sidenote: I’m experimenting with Alpine right now), I think
I had to make the realisation that no matter how hard you’re trying, the “perfect” system does not
exist. As with everything in life, it’s all a matter of compromises.&lt;/p&gt;
&lt;p&gt;However, it’s up to you to decide on the compromises that are made. Is it important that the whole
state of the system is persisted in some kind of Git repository, fully reproducible? Then &lt;em&gt;NixOS&lt;/em&gt;
might be for you. However, if you’re not a fan of Nix itself, then maybe use something like
&lt;code&gt;rpm-ostree&lt;/code&gt;. Other compromises have to be made for that.&lt;/p&gt;
&lt;p&gt;I aim for &lt;strong&gt;low maintenance&lt;/strong&gt;. And for this, I have to make the compromise that I might not have the
whole state described in some repository. Ansible, after all, also has it’s limitations. &lt;strong&gt;But
that’s okay. I came to my mind with that.&lt;/strong&gt; The
&lt;a href=&quot;https://jasminchen.dev/notes/2025/system-administration-still-sucks/&quot;&gt;rant from earlier this year&lt;/a&gt; still applies to a
certain extent and I’m still convinced that there’s an easier (more manageable) solution than
Ansible out there.&lt;/p&gt;
&lt;h2 id=&quot;the-philosophical-perspective&quot;&gt;The philosophical(?) perspective&lt;/h2&gt;
&lt;p&gt;Maybe the reason, why we (as in system administratiors and software engineers) strive so much for
perfectionism, is because our whole world is just virtual. We do not resolder our mainboards every
two years, we don’t reinvent bridges or tables or everything else out there every year or so. That
stuff just works. If it doesn’t, it can be easily replaced/fixed. And even for big things (thinking
of a skyscraper), you do the engineering once and then you’re done. No one expects you to rebuild
the whole thing from wood or bamboo, because it’s suddenly cheaper than concrete. In our world, it’s
however (more or less) required to do so.&lt;/p&gt;
&lt;p&gt;Maybe the tech industry is also very very bad at teaching. I mean, we have how many tools for
managing servers? I could easily name five. Likely even more. Have I used any of them? Nah. I heard
of them and know that they’re the “old” way to manage servers (thinking specificially about Puppet
and Chef here) and that all most of the folks out there use Ansible now. If you have the need to get
everything as a managed cloud service, you might even manage everything inside OpenTofu files. It’s
just a very, &lt;em&gt;very&lt;/em&gt; expensive system then.&lt;/p&gt;
&lt;p&gt;Another example: Recently I read the
&lt;a href=&quot;https://erlang.org/download/armstrong_thesis_2003.pdf&quot;&gt;PhD study of Joe Armstrong (2003)&lt;/a&gt;, one of
the co-inventors of the &lt;em&gt;Erlang&lt;/em&gt; programming language. It’s titled “Making reliable distributed
systems in the presence of software errors” and reading it was such a delightful experience. Erlang,
if you aren’t familiar with the programming language, was designed for scalability in… 1989. And
if you look at the paper, closely at page 27, the expectations of stability were “defined” as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Typically having less than two hours of down-time in 40 years&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I have to say, I have enormous respect for Mr Armstrong and other folks from that era. The sheer
computation limitations led to some extremely interesting ideas and solutions. They didn’t have to
invent Kubernetes to run a mobile network. Computation power was expensive then and so, different
compromises had to be made.&lt;/p&gt;
&lt;p&gt;Not saying that everything was better then, but the tech industry really seemed to have taken more
care back then, when computers we’re not that powerful and cheap as they are nowadays. What I’m
trying to say? I don’t know either. I guess, it might be a good time to learn some old stuff again.
To learn from and more about the early beginnings of the tech industry.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Choosing an jujutsu revision interactively using fish and fzf</title>
    <link href="https://jasminchen.dev/notes/2025/choosing-an-jujutsu-revision-interactively-using-fish-and-fzf/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>5e12a3927a90e27fe0bd4bfbe9720cf3ba5a5727</id>
    <content type="html">&lt;p&gt;During optimizations of my fish configuration, I wanted to make it easier for me to choose revisions
in jujutsu. They are &lt;em&gt;the&lt;/em&gt; core component of my workflow, but choosing them, even with the already
provided dynamic completions was sometimes rather cumbersome.&lt;/p&gt;
&lt;p&gt;The existing workflow required me to open the log &lt;em&gt;and&lt;/em&gt; to remember the change ID I was going to
work with. This “worked”, but it was annoying me and especially late in the day, it simply was too
error prone.&lt;/p&gt;
&lt;p&gt;As a consequence, I checked the
&lt;a href=&quot;https://github.com/jj-vcs/jj/wiki/fzf&quot;&gt;fzf section in the jujutsu wiki&lt;/a&gt; and at the end of the wiki,
there’s a link to a little
&lt;a href=&quot;https://gist.github.com/tdaron/f5d0985687d8aed06714c8901dfb5fcb&quot;&gt;gist that allows for interactive choosing of revisions&lt;/a&gt;.
It was not &lt;em&gt;quite&lt;/em&gt; sufficient for me at the time of writing, for example, it only allowed to be
executed in the root of the repository.&lt;/p&gt;
&lt;p&gt;After some smaller adjustments, this is what I came up with:&lt;/p&gt;
&lt;pre class=&quot;language-fish&quot;&gt;&lt;code class=&quot;language-fish&quot;&gt;function fzf_jj_choose --description &quot;Interactive choosing of a jj revision using fzf&quot;
    # Source for the original idea: https://gist.github.com/tdaron/f5d0985687d8aed06714c8901dfb5fcb
    set -l template &#39;
        change_id.shortest()
        ++ &quot;&#92;t&quot; ++ description.first_line()
        ++ &quot; &quot; ++ bookmarks.join(&quot; &quot;)
        ++ &quot;&#92;n&quot;
    &#39;

    # Check whether we&#39;re inside a jj repo
    jj root --quiet &amp;&gt;/dev/null; or return

    set -l name (jj log --no-graph -T $template --color always | fzf --ansi | cut -f1)
    commandline -it &quot;$name&quot;
end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is placed in &lt;code&gt;.config/fish/functions/fzf_jj_choose.fish&lt;/code&gt; and needs a new keybinding,
which is added to the fish config. For me, it’s in the &lt;code&gt;.config/fish/conf.d/jj.fish&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-fish&quot;&gt;&lt;code class=&quot;language-fish&quot;&gt;# Bind Ctrl+J to interactively select a revision using fzf.
bind ctrl-j fzf_jj_choose&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that binding in place, I can just press &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;J&lt;/kbd&gt; at any given time and
select a jujutsu revision ID with a breeze. &#92;o/&lt;/p&gt;
&lt;p&gt;Again, big thanks to &lt;a href=&quot;https://theo.daron.be/&quot;&gt;Théo&lt;/a&gt; for the original idea!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Smaller feed adjustments have been made</title>
    <link href="https://jasminchen.dev/notes/2025/feed-adjustments/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>ca43fd1a3685f3b1feb8dcabdbc196d08d8468ce</id>
    <content type="html">&lt;p&gt;If there were a lot of older posts appearing in your feed reader today, sorry!&lt;/p&gt;
&lt;p&gt;It’s fixed now, the post identifiers are now stable, thanks to the
&lt;a href=&quot;https://bobmonsour.com/blog/final-final-word-on-rss-entry-ids/&quot;&gt;helpful ideas of Bob&lt;/a&gt;, so it won’t
happen again (famous last words lol).&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>ZFS dataset decryption using the TPM with the help of systemd</title>
    <link href="https://jasminchen.dev/notes/2025/zfs-dataset-decryption-using-the-tpm-with-the-help-of-systemd/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>b10b16a5e4118c6f7fd23629cbf28eaea1552fc1</id>
    <content type="html">&lt;p&gt;On my/our homeserver, the pretty awesome ZFS filesystem is used for the data. Because this is the
first experiment with ZFS, it’s not the whole pool that is encrypted, just individual datasets. This
is especially important for information like the documents, that are are stored in
&lt;a href=&quot;https://docs.paperless-ngx.com/&quot;&gt;paperless-ngx&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Because those were my first experiments, the ZFS encryption was made via passphrases instead of
specific files on the disk. This, however, had one big caveat: On each reboot, those keys had to be
entered again using &lt;code&gt;zfs load-key&lt;/code&gt;. It is possible to use raw keys that are stored on the disk
instead of using passphrases.&lt;/p&gt;
&lt;p&gt;But, there’s one problem with the current setup. The disk where those keys would be stored on, is
unencrypted at the moment. Yes, yes, consider your threat model yadda yadda, I know. It wouldn’t be
much of a problem to just leave it there, but &lt;em&gt;it doesn’t feel right&lt;/em&gt;. I considered using a YubiKey
or setup some webserver for entering those keys, like
&lt;a href=&quot;https://words.filippo.io/dispatches/frood/&quot;&gt;Filippo Valsorda did it with his NAS frood&lt;/a&gt; (really can
recommend the article, I like his approach a lot!).&lt;/p&gt;
&lt;p&gt;The next idea was the TPM and I remembered that I used this already to setup automatic decryption on
all my other devices with
&lt;a href=&quot;https://www.man7.org/linux/man-pages/man1/systemd-cryptenroll.1.html&quot;&gt;&lt;code&gt;systemd-cryptenroll&lt;/code&gt;&lt;/a&gt;. But…
that’s just for LUKS volumes, weh. Yet, systemd can leverage the TPM, so I continued searching and
found the page about &lt;a href=&quot;https://systemd.io/CREDENTIALS/&quot;&gt;systemd credentials&lt;/a&gt; and therefore found out
about the &lt;code&gt;systemd-creds&lt;/code&gt; tool!&lt;/p&gt;
&lt;p&gt;That’s exactly what I need! Credentials aka the dataset passphrases, encrypted and decrypted via the
TPM, making access to the disk worthless without running it inside the computer that it’s desired to
run in. Awesome!&lt;/p&gt;
&lt;h2 id=&quot;systemd-to-the-rescue&quot;&gt;systemd to the rescue!&lt;/h2&gt;
&lt;p&gt;ZFS mounts can be generated via the &lt;code&gt;zfs-mount-generator&lt;/code&gt;, which
&lt;a href=&quot;https://wiki.archlinux.org/title/ZFS#zfs-mount-generator&quot;&gt;needs a bit of setup upfront&lt;/a&gt;. After
that, we can make use of the &lt;code&gt;zfs-load-key@.service&lt;/code&gt;, which itself is automatically loaded for all
ZFS datasets that are encrypted aka &lt;code&gt;zfs get encryption &amp;lt;dataset&amp;gt;&lt;/code&gt; doesn’t return &lt;code&gt;off&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The templating of units becomes really useful here to share a common behaviour: For example, the
dataset &lt;code&gt;tank/documents&lt;/code&gt; would automatically depend on &lt;code&gt;zfs-load-key@tank-documents.service&lt;/code&gt;. But
since I planned to use the same approach for decryption of all my datasets, I can just override the
default behaviour for all invocations of &lt;code&gt;zfs-load-key@.service&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Long story short, after a lot of tinkering, I came up with the following systemd dropin:&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# /etc/systemd/system/zfs-load-key@.service.d/import-key-via-systemd-creds.conf&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# This overrides the default loading mechanism for ZFS dataset keys and allows them&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# to be placed on disk.&lt;/span&gt;
&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Required to be set, otherwise the encrypted keys on disk are not available on boot yet.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;RequiresMountsFor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/etc/credstore.encrypted/&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Loads a credential with the same name as the dataset mount.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;LoadCredentialEncrypted&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;%i&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Remove the default implementation…&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# … and replace it with systemd-creds.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/bin/sh -euc &#39;systemd-creds cat %i | zfs load-key $(systemd-escape -u %i)&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above snippet loads the credential from the credential store, e.g.
&lt;code&gt;/etc/credstore.encrypted/tank-documents&lt;/code&gt;, outputs it (since it’s a passphrase) via &lt;code&gt;systemd-creds&lt;/code&gt;
and passes this to &lt;code&gt;zfs load-key&lt;/code&gt;. The &lt;code&gt;systemd-escape&lt;/code&gt; in there is required to transform the
escaped value &lt;code&gt;tank-documents&lt;/code&gt; into a proper path &lt;code&gt;tank/documents&lt;/code&gt;, which is the format required by
the &lt;code&gt;zfs&lt;/code&gt; CLI.&lt;/p&gt;
&lt;h2 id=&quot;example-usage&quot;&gt;Example usage&lt;/h2&gt;
&lt;p&gt;In order to use it, the key has to be persisted in the credentials store. In bash, this would be
achieveable like this:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Note: all of the commands below assume that you&#39;re root.&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;DATASET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;tank/documents&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# First, create the dataset. `zfs` is going to ask for the passphrase.&lt;/span&gt;
zfs create &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;encryption&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;on &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;keyformat&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;passphrase &lt;span class=&quot;token variable&quot;&gt;$DATASET&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Next, we store the passphrase in the local credential store, encrypted only(!) via the TPM.&lt;/span&gt;
systemd-ask-password &lt;span class=&quot;token string&quot;&gt;&quot;Encryption phrase for &lt;span class=&quot;token variable&quot;&gt;$DATASET&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; systemd-creds encrypt --with-key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;tpm2 - &lt;span class=&quot;token string&quot;&gt;&quot;/etc/credstore.encrypted/&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;systemd-escape &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; $DATASET&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important line is the invocation of the &lt;code&gt;systemd-creds&lt;/code&gt; command, which will read the passphrase
from stdin and store it in the &lt;code&gt;/etc/credstore.encrypted&lt;/code&gt; folder.&lt;/p&gt;
&lt;h3 id=&quot;important-note&quot;&gt;Important note&lt;/h3&gt;
&lt;p&gt;The files can only(!) be decrypted on the same host. A backup of the key is useless, you have to
store the passphrase somewhere else, if you want to recover the data in the future on a different
host.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;And that’s it! With that I was able to just use the existing passphrases and store them in the TPM.
On reboots, the datasets are now automatically decrypted and I no longer need to enter the
passphrases manually.&lt;/p&gt;
&lt;p&gt;And yes, the above ideas can be adjusted to use encrypted raw keys as well. Instead of
&lt;code&gt;systemd-creds cat&lt;/code&gt; it’ll likely be something along the lines of just using &lt;code&gt;zfs load-key&lt;/code&gt; together
with the &lt;code&gt;$CREDENTIALS_DIRECTORY&lt;/code&gt; environment variable.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Sieve filter to auto-sort mails from Git forges</title>
    <link href="https://jasminchen.dev/notes/2025/sieve-filter-to-auto-sort-mails-from-git-forges/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>585675feb7a33e8b46ae0e47c276b15c5fbcdcd8</id>
    <content type="html">&lt;p&gt;Because I’m interacting with a lot of open source projects online (both at work and in private), I
also get a lot of mails being sent by GitHub. And because I’m a member of the
&lt;a href=&quot;https://codeberg.org&quot;&gt;Codeberg e.V.&lt;/a&gt; as well, where a lot of discussion is happening via issues on
private projects, my mailbox became cluttered over time.&lt;/p&gt;
&lt;p&gt;Filtering out mails from Dependabot &amp;amp; Renovate already helped with the noise, yet I was overwhelmed
by it a lot of the times. Luckily, my mail provider, &lt;a href=&quot;https://migadu.com/&quot;&gt;Migadu&lt;/a&gt; (which I can only
recommend!), supports &lt;em&gt;Sieve&lt;/em&gt; scripting.&lt;/p&gt;
&lt;p&gt;I won’t go into detail here how to use them and what’s supported, but with the following Sieve
script, I’m able to automatically sort all my incoming mails into proper folders.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &amp;quot;variables&amp;quot;;
require &amp;quot;fileinto&amp;quot;;

if header :matches &amp;quot;List-ID&amp;quot; &amp;quot;*/* &amp;lt;*.github.com&amp;gt;&amp;quot;
{
    fileinto &amp;quot;Code/${1} | ${2}&amp;quot;;
} elsif header :matches [&amp;quot;X-Forgejo-Repository-Path&amp;quot;, &amp;quot;X-Gitea-Repository-Path&amp;quot;, &amp;quot;X-GitLab-Project-Path&amp;quot;] &amp;quot;*/*&amp;quot;
{
    fileinto &amp;quot;Code/${1} | ${2}&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The folder pattern is &lt;code&gt;{org} | {project}&lt;/code&gt;. An earlier version combined them into both, but this was
rather tedious because of all the unnecessary folders in between. The default separator is &lt;code&gt;/&lt;/code&gt; in
URLs and it’s also in mailboxes, which means that for each organisation a folder was created, with a
project folder below that.&lt;/p&gt;
&lt;p&gt;That was pretty annoying to me, so I switched that to the vertical bar.&lt;/p&gt;
&lt;p&gt;From time to time I’m also using
&lt;a href=&quot;https://gitlab.com/puzzlement/delete-empty-imap-dirs&quot;&gt;this script to clean up empty IMAP folders&lt;/a&gt;,
which does the job astonishingly good.&lt;/p&gt;
&lt;p&gt;And yea, that’s it! Now I have all my Git-related mails cleanly organised in subfolders below
&lt;code&gt;Code/&lt;/code&gt; :3&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>hamsterradeln</title>
    <link href="https://jasminchen.dev/notes/2025/hamsterradeln/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>f6dab54552c43c9070242577aab691fae19cc4dd</id>
    <content type="html">&lt;p&gt;&lt;em&gt;hamsterradeln&lt;/em&gt;, Verb.&lt;/p&gt;
&lt;h2 id=&quot;bedeutung&quot;&gt;Bedeutung&lt;/h2&gt;
&lt;p&gt;Ausdruck der Monotonie des Alltags und das Auslassen des Frusts darüber.&lt;/p&gt;
&lt;h2 id=&quot;herkunft&quot;&gt;Herkunft&lt;/h2&gt;
&lt;p&gt;Wortneuschöpfung aus dem Substanstiv &lt;a href=&quot;https://de.wiktionary.org/wiki/Hamsterrad&quot;&gt;Hamsterrad&lt;/a&gt; und dem Verb &lt;a href=&quot;https://de.wiktionary.org/wiki/radeln&quot;&gt;radeln&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;beispiel&quot;&gt;Beispiel&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Morgen wieder hamsterradeln, gar keine Lust.&lt;/li&gt;
&lt;li&gt;Da sie morgen hamsterradeln wird, ist sie heute kraftlos.&lt;/li&gt;
&lt;/ol&gt;
</content>
  </entry>
  <entry>
    <title>System administration of Linux servers still sucks</title>
    <link href="https://jasminchen.dev/notes/2025/system-administration-still-sucks/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>24dbb5807fc16ccdd18dfda0c4679494d5c6f4db</id>
    <content type="html">&lt;p&gt;When playing with the homelab (and my other servers that I’m administering), I realize again and
again that system administration done right ™ still sucks. What I mean by doing right? &lt;strong&gt;Properly
documented.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It’s fairly easy to get a system running and do whatever I want to do with that. Download the
ISO/qcow2, boot it, login, and do anything. However, over time, the system begins to become dirty.
Packages, only needed for a little experiment, configuration files that are no longer needed, users
and heck, even state in &lt;code&gt;/var/lib&lt;/code&gt; is going to clutter the system.&lt;/p&gt;
&lt;p&gt;And as the server gets older (installation-wise), that’s inevitable.&lt;/p&gt;
&lt;h2 id=&quot;ansible&quot;&gt;Ansible&lt;/h2&gt;
&lt;p&gt;But: There are solutions to work around those. Ansible, &lt;em&gt;the&lt;/em&gt; most common known tool (I think) for
configuring servers allows for a &lt;em&gt;relatively&lt;/em&gt; easy configuration using so called &lt;em&gt;playbooks&lt;/em&gt;. A
playbook here, a playbook there, and suddenly, the whole server is set up.&lt;/p&gt;
&lt;p&gt;Eeeeexcept, it still doesn’t avoid that you just login and do stuff &lt;em&gt;outside&lt;/em&gt; of Ansible. And so the
problem remains. Even at work, where we’re having a fully-fledged architecture full of roles,
playbooks, even Ansible AWX hosted by another team, this was inevitable. Of course, you can always
blame the persons maintaining the roles and the playbooks of not using them &lt;em&gt;more&lt;/em&gt;. But I think
that’s a bit too easy. Ansible, although it tries to reduce the friction a lot, is still friction.
Maybe not so much for installing arbitrary packages, but as soon as it involves a bit of scripting,
it’s becoming cumbersome. You have to understand the Jinja2 templating language, you have to make
sure that the playbook is doing what it should do and all that. Not to mention that Ansible is a
software that you need to update and oh boy, they’re releasing quite a lot of major releases.&lt;/p&gt;
&lt;p&gt;Does the content of the playbook change? Not at all.&lt;/p&gt;
&lt;p&gt;Yet, now you’re no longer maintaining just a server, but the setup infrastructure as well. This
might be a valid compromise if you’re maintaining a fleet of servers and you have to make sure that
everything is running as intended or the company loses money, no doubt. But for me, who’s just
maintaining a handful of servers which are completely independent from each other? &lt;strong&gt;It’s not worth
it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Also, it’s getting fun if you have to delete stuff again. Just deleting the stuff from the playbook
won’t do it, you have to actively invert the action. Usually it’s “just” rewriting legacy stuff to
&lt;code&gt;state: absent&lt;/code&gt;, but there might be more involved.&lt;/p&gt;
&lt;h2 id=&quot;nixos&quot;&gt;NixOS&lt;/h2&gt;
&lt;p&gt;Let’s have a look at the second contender in the room: &lt;a href=&quot;https://nixos.org/&quot;&gt;NixOS&lt;/a&gt;. Based on their
homepage, Nix&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[…] is a tool that takes a unique approach to package management and system configuration.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And that certainly is true. NixOS is built on the Nix language. And although you can do &lt;em&gt;a lot&lt;/em&gt; with
it, it’s also another thing to learn. Not that I’m disliking the fact that I have to learn a new
thing, nope, not at all. But the last time, I looked at it, it didn’t allow me to learn the way I
intended to. It might be a fact of the changing times in the Nix community, but when I searched for
Nix tutorials back then, I found more problems that Nix users are having than actual solutions.&lt;/p&gt;
&lt;p&gt;The videos on the homepage are great and they make it look soo easy to understand what’s going on.
On the other hand is the actual maintenance work. And based on my observation from the folks in the
Fediverse that I follow, maintaining NixOS-based systems is not &lt;em&gt;that&lt;/em&gt; easy and requires &lt;strong&gt;a lot&lt;/strong&gt;
of processing power. And the fact that there’s a major release every six months also drove me away
from even trying it. I use(d) Nix for development environments, but even there, it more and more
became a maintenance burden. Not to mention that telling “just install Nix” to my coworkers is more
an insult than actually helpful.&lt;/p&gt;
&lt;p&gt;I have respect for everyone who’s using Nix to manage their systems. It’s just not for me and that’s
okay.&lt;/p&gt;
&lt;h2 id=&quot;kubernetes&quot;&gt;Kubernetes&lt;/h2&gt;
&lt;p&gt;One might argue that Kubernetes is not a configuration management tool, but to be honest, it kinda
is. You “just” need a system to run it on. And that, well, must be configured. (Not to mention the
whole complexity of control planes, etcd backups, whatever.)&lt;/p&gt;
&lt;p&gt;It’s not a lack of knowledge for me, I use Kubernetes at work the whole time. Do I want to have it
on my own systems? Not really. It’s good, definitely has it’s usecases, but is much more maintenance
than I want to have in my homelab.&lt;/p&gt;
&lt;h2 id=&quot;ostree&quot;&gt;OSTree&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ostreedev.github.io/ostree/&quot;&gt;OSTree&lt;/a&gt; is a rather “old” technology out there. And somehow,
it also never took off. Why? I don’t know. Maybe there’s too much setup needed upfront, maybe it’s
too complicated. Or maybe, people are really angry at Red Hat for some reason, I don’t know.&lt;/p&gt;
&lt;p&gt;Yet, it’s one of the things where I appreciate the ideas behind it. It certainly &lt;em&gt;is&lt;/em&gt; useful I think
and the fact that it’s used for the immutable Fedora distributions also kinda undermines that it’s
useful in production.&lt;/p&gt;
&lt;p&gt;But: The last time I used it (Fedora 39 &amp;amp; Fedora 40)
(&lt;a href=&quot;https://jasminchen.dev/notes/2024/goodbye-arch/&quot;&gt;see also the blog post back then&lt;/a&gt; it was really really slow. Not sure
whether that was due to the LUKS encryption on there or because my old ThinkPad was seeing it’s age.
(the heck, it was from 2017, it’s not &lt;em&gt;that&lt;/em&gt; old). It was noticeable. I’m writing this words on a
ThinkPad running Fedora Workstation (the regular, old-fashioned way of running Fedora) and the &lt;code&gt;dnf&lt;/code&gt;
updates are never slow. They are usually done pretty quickly. And also done in one transaction at
startup, with a Windows-like “Updating your system, please wait” splashscreen. It’s not perfect, but
&lt;strong&gt;it’s good enough&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Because of the reasons above, I’ve ditched OSTree. Until I came back to it recently, because of
&lt;em&gt;bootable containers&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;bootable-containers&quot;&gt;Bootable containers&lt;/h2&gt;
&lt;p&gt;If you haven’t been living under a rock, you have heard of the terms “container” before. Although
it’s just a number of tarballs together with some metadata combined, it changed the way of
sysadmins, whether they like it or not.&lt;/p&gt;
&lt;p&gt;And now, Red Hat, likely due to it’s work on it’s Kubernetes variant called &lt;em&gt;OpenShift&lt;/em&gt;, wants to
bring this to operating systems as well. It’s not new magic, it’s basically OSTree, but with
containers as the infrastructure to distribute the images. I do why they’re trying to build it.
Container registries, much like S3 buckets, are the things that are so ubiquitous in today’s &lt;em&gt;Cloud
Native&lt;/em&gt; ecosystem that they’re already present. Managing a new format, like for OSTree can be seen
as a mainteance burden not strictly required.&lt;/p&gt;
&lt;p&gt;In a Kubernetes environment, where it’s not that uncommon, to have a so-called “machine controller”
in your cluster, delivering whole operating systems via containers makes sense.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just as a heads-up: a machine controller in a Kubernetes cluster is responsible for the whole
lifecycle of a VM. It’s talking to your cloud providers API, spawns a new VM, registers it as a
node and also ensures it’s deleted via the Kubernetes primitives.&lt;/p&gt;
&lt;p&gt;If it’s setup in your cluster, it is, in fact, pretty pretty nice to work with. All machines
managed by that are basically the same, except for minor things like the IP addresses, the
hostname and that. The rest of it? Complementely managed by Kubernetes. Which also means that you
don’t need SSH on those machines and can isolate them pretty good from the outer network!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you’re trans and you need an argument to look into it, it’s core component &lt;a href=&quot;https://bootc-dev.github.io/bootc/&quot;&gt;bootc&lt;/a&gt; is &lt;em&gt;whispers&lt;/em&gt;
written in ✨Rust✨.&lt;/p&gt;
&lt;p&gt;My experience: &lt;code&gt;Containerfile&lt;/code&gt;s aren’t still my favorite, not even with the introduction of
heredoc-like strings in &lt;code&gt;RUN&lt;/code&gt; commands. They’re usable, sure. But for managing a whole operating
system, it is a bit cumbersome to me. Also: Container registries &lt;em&gt;are&lt;/em&gt; pretty common and you can get
several ones for free. But: Why do I have to rely on an external dependency to update my systems
now? Sure, I can rebuild the container nightly via GitHub Actions and with proper caching it should
be pretty fast, but: Is all of that &lt;em&gt;really&lt;/em&gt; neccessary? That’s a lot of infrastructure for the two
or three servers I’m having.&lt;/p&gt;
&lt;p&gt;And that’s also the reason why I ditched the idea of using them.&lt;/p&gt;
&lt;h2 id=&quot;what-do-i-actually-want&quot;&gt;What do I actually want?&lt;/h2&gt;
&lt;p&gt;Actually, I’m not sure about that either. You know how Windows has a thing like “Reset, but keep
applications and user data”? I want that as well. I want to get rid of all the files that might have
cluttered my &lt;code&gt;/var&lt;/code&gt; in the meantime, I just wanna keep the stuff that’s relevant.&lt;/p&gt;
&lt;p&gt;I want a system where I have a history of what I’ve done in the past. Which files I created, which
services I modified, all that. And it’s all tidied up somewhere in a Git repository, where I can
then annotate it and where it’s getting synced back from. Yes, I want bidirectional sync.
&lt;a href=&quot;https://etckeeper.branchable.com/&quot;&gt;etckeeper&lt;/a&gt; does one half of the job, it just won’t work for my
Fedora systems
&lt;a href=&quot;https://etckeeper.branchable.com/forum/RFE:_please_add_support_for_DNF_5/&quot;&gt;because of lack of support for dnf5&lt;/a&gt;.
:c&lt;/p&gt;
&lt;p&gt;I want a &lt;em&gt;Time Machine&lt;/em&gt;-like experiences for my servers. I want &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt;. I just wanna
hop onto a server, start my &lt;code&gt;start-recording&lt;/code&gt; script, do whatever the fuck I can imagine and then,
when I’m done, I stop the recording and I receive a wonderful summary of all the things I’ve done.&lt;/p&gt;
&lt;p&gt;And the output can be an Ansible playbook, a Nix module, whatever. When I invoke &lt;code&gt;useradd&lt;/code&gt;, I want
to configure when, at the end of my session, an actual entry to &lt;code&gt;/etc/shadow&lt;/code&gt; is written or maybe
something like
&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd-sysusers.html&quot;&gt;systemd-sysusers&lt;/a&gt;
file. I want that the computer figures out how I can document the things I’ve done.&lt;/p&gt;
&lt;p&gt;And even if all I get is a plain directory full of the changes I’ve done, that’d be enough for me. I
just want to avoid that state of either really fucking up your system by accident, using all the CPU
cycles to rebuild it (whether it’s a container or a Nix flake doesn’t matter for this argument)
— I just want to play with my computer and know what I’ve done on them. I don’t need to track
every little patch update that is done. I only want to keep the info that I installed vim and if,
&lt;em&gt;if&lt;/em&gt;, I really want to pin the version, every package manager on this planet provides me some way to
do this.&lt;/p&gt;
&lt;p&gt;All of the above is possible and probably pretty easy to implement on filesystems that support
snapshots. It’s just wild to me that it’s apparently 2025 and closest thing we’re having are
auto-snapshots (via btrfs, e.g. in openSUSE). We could make our computers could assist us much
better in that regard.&lt;/p&gt;
&lt;p&gt;and yet, we degrade them to produce slop. because number go up. and that sucks.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>&quot;Clean code&quot; is not an excuse to avoid writing comments</title>
    <link href="https://jasminchen.dev/notes/2025/clean-code-as-an-antipattern/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>b09d7e9bf989e7074c09499d87e3737acf0d6644</id>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;Good code does not need documentation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This quote, appearing in several forms over the last couple of weeks in my professional and personal
life have been driving me nuts. Usually it is not phrased exactly like that, but more as a question
that goes in the following direction:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Why do I have to write documentatiom, it’s clear based on the code alone?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My opinion: &lt;strong&gt;This statement never holds true.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You might say that &lt;em&gt;domain-driven design&lt;/em&gt;, properly named variables and a good software architecture
already is enough documentation. And that there is no need to comment the following piece of Go
code:&lt;/p&gt;
&lt;pre class=&quot;language-go&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;queryInvoiceByID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ctx context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Context&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; id &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;Invoice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// implementation omitted for now&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you don’t know Go: The function above takes two parameters and returns a pointer to an &lt;code&gt;Invoice&lt;/code&gt;
struct, together with an error. However, the programming language is not important for my point
here. You can replace that function with any other in your mind.&lt;/p&gt;
&lt;p&gt;So, what does the code do? Right now, we cannot know, as I haven’t provided any implementation. The
next question probably is: &lt;em&gt;What do you expect it to do?&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;test-driven-development-to-the-rescue&quot;&gt;Test-Driven Development to the rescue!&lt;/h2&gt;
&lt;p&gt;If you’ve been programming for a while, you probably heard of &lt;em&gt;Test-Driven Development&lt;/em&gt;, commonly
abbreviated as &lt;em&gt;TDD&lt;/em&gt;. The idea is simple: Write down the requirements as tests and only after then,
write only the necessary amount of code required for the tests to pass. In an ideal world, tests
translate to business requirements.&lt;/p&gt;
&lt;p&gt;And in a lot of tutorials out there, the authors also argument that this documents the code. If we
have a test called &lt;code&gt;TestQueryInvoiceByIDReturnsInvoiceIfInDatabase&lt;/code&gt;, we know that this method
returns the invoice if it’s in the database. However, a question to you, dear reader? Does this
actually provide additional value for the understanding of the function? To me, it does not. Not at
all. It introduces a new external component, called “database”. But apart from that, that’s it.&lt;/p&gt;
&lt;p&gt;So, there are a lot of questions that are still unanswered. I haven’t provided any additional
context to the system where this function is coming from. As readers, we need that to understand
this gear in the other set of gears! Invoices are everywhere, so the code snippet above could also
be taken from almost any application that has to manage invoices somehow. It can be the interface of
a cloud provider, from the website of your local bike shop or even from a system like
&lt;a href=&quot;https://docs.paperless-ngx.com/&quot;&gt;paperless-ngx&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;improvement-no-1-the-bare-minimum&quot;&gt;Improvement No. 1: The bare minimum&lt;/h2&gt;
&lt;pre class=&quot;language-go&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// queryInvoiceByID queries the Invoice by the id.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;queryInvoiceByID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ctx context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Context&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; id &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;Invoice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If another developer would read this, they would respond with a “no shit, sherlock” reaction. Does
this comment provides a benefit? Not really. But: There’s a lot of possible improvement here!&lt;/p&gt;
&lt;h2 id=&quot;the-meaning-of-comments-in-code&quot;&gt;The meaning of comments in code&lt;/h2&gt;
&lt;p&gt;Comments in code are not limited to the same boundaries as the code itself. Comments can use
metaphors, links to external resources, maybe even little ASCII diagrams. Good comments are art, to
a certain extent. They can express ideas, thoughts and even little musings for the next reader!&lt;/p&gt;
&lt;p&gt;When writing comments, I try to include as much information as possible. In the past, the following
things ended up as comments in my code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Behaviour in error cases, esp. if it’s deriving from the norm&lt;/li&gt;
&lt;li&gt;Ticket numbers that introduced it&lt;/li&gt;
&lt;li&gt;External limitations that influence the implementation&lt;/li&gt;
&lt;li&gt;Earlier implementation ideas that did not work out (and the reason why!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One could argue that this partially belongs in the commit message. I don’t disagree! A lot of those
things can also end up in the detailed description of your commit, but it doesn’t exclusively have
to be there. If in doubt, I always stick to the code itself as the place to write down details for
behaviours. That way it’s easily there for anyone to read and not hidden somewhere in the Git
history.&lt;/p&gt;
&lt;p&gt;No doubt, this is a lot. If you’re not used writing documentation for your code, this can feel
&lt;strong&gt;very&lt;/strong&gt; overwhelming. Admittedly, I struggled with this for a huge time of my career as well. In
the past, I was arguing in favor of refactorings over writing documentation. I always assumed that
this should be enough.&lt;/p&gt;
&lt;p&gt;But this was from a time where I was working mostly alone on my projects. Working more and more in
teams (aka in companies, instead of my free time), this started to shift. I caught myself
communicating my intents as comments more often. I started to add explanations to bash scripts that
invoked external commands. In the end, it’s easier to just read a comment right next to it than to
decompose what &lt;code&gt;rsync -aivtS&lt;/code&gt; is doing under the hood.&lt;/p&gt;
&lt;p&gt;And to be honest, we, as developers, should have &lt;strong&gt;clear communication as our primary goal&lt;/strong&gt;.
“Clean” code, how good the intentions of the programmer were, will never reach that.&lt;/p&gt;
&lt;h2 id=&quot;improvement-no-2-the-detailed-comment&quot;&gt;Improvement No. 2: The detailed comment&lt;/h2&gt;
&lt;p&gt;So, how could a “good” method be annotated?&lt;/p&gt;
&lt;pre class=&quot;language-go&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// queryInvoiceByID queries the invoice and its associated metadata by its id from the database.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Callers must ensure that the transaction is opened and attached to the context.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// If it cannot be found, an ErrNotFound is returned.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// For other errors, the underlying error is wrapped before.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Note: Although invoices are having an UUIDv4 ID nowadays, a string is required here to ensure&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// backwards-compatibility with the old invoice system.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;queryInvoiceByID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ctx context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Context&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; id &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;Invoice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Although passing complex parameters (like transactions) via the context might be considered an&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// anti-pattern in Go, it is used here to extract common behaviour.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// This should be removed in the future as part of the database overhaul (PROJ-1337).&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// ...rest of the implementation&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice something? I still haven’t provided even just one line of implementation, yet readers already
know what to expect and what to take care of when calling this function. Additional bonus feature:
Because it’s a comment, future programmers don’t have to read the code at all when
&lt;code&gt;queryInvoiceByID&lt;/code&gt; is called somewhere. They can rely on their editors assistance, which can display
the comment when hovering the function! I know I am repeating myself, but: Providing only code and
tests – which all have to be parsed! – cannot provide this.&lt;/p&gt;
&lt;p&gt;Does this take practices? You bet it does. But it’ll make your job and that of others a lot easier,
so: let’s do it together! Word by word, phrase by phrase, let’s write comments! ✍️&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Automatically enable auto-merge for all merge requests on GitLab</title>
    <link href="https://jasminchen.dev/notes/2025/automerge-for-all-mrs-on-gitlab/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>8cfdb5f49fdb66cd97431d961a1dfb4f83cc911b</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://about.gitlab.com/releases/2024/09/19/gitlab-17-4-released/&quot;&gt;GitLab 17.4&lt;/a&gt;, released in September 2024, introduced a very welcome change to its auto-merging
feature.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GitLab now supports Auto-merge for all checks in merge requests. Auto-merge enables any user who
is eligible to merge to set a merge request to Auto-merge, even before all the required checks
have passed. As the merge request continues through its lifecycle, the merge request automagically
merges after the last failing check passes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When I read this first, my eyes were immediately wide open. Up to that point, our merge request
workflow at work was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Person 1 opens the merge request (MR)&lt;/li&gt;
&lt;li&gt;Person 2 approves it&lt;/li&gt;
&lt;li&gt;Person 1 merges it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The time between step 2 and 3 could be a couple of seconds, but we observed several days as well.
Pretty cumbersome if you ask me. And what’s the developers’ solution to a manual and repetitive
task? You guessed it, automations!&lt;/p&gt;
&lt;h2 id=&quot;first-problem-the-merge-request-rest-api-is-not-ready-for-auto-merge&quot;&gt;First problem: The Merge Request REST API is not ready for auto-merge&lt;/h2&gt;
&lt;p&gt;The
&lt;a href=&quot;https://docs.gitlab.com/api/merge_requests/#merge-a-merge-request&quot;&gt;REST API documentation as a dedicated &lt;code&gt;/merge&lt;/code&gt; endpoint&lt;/a&gt;,
with a &lt;code&gt;merge_when_pipeline_succeeds&lt;/code&gt; parameter. My first idea was to use this API, but in my tests
I always received a &lt;code&gt;405 Method Not Allowed&lt;/code&gt; error. According to the documentation, this means that
the MR cannot be merged. Technically, this is correct, yet I hoped that it’d automatically trigger
the auto-merge functionality.&lt;/p&gt;
&lt;p&gt;It did not. I actually do not blame GitLab for this. In retrospect, implicit behaviour like the
auto-merge feature would be a change in behaviour and therefore might’ve introduced unexpected
side-effects for existing API users.&lt;/p&gt;
&lt;p&gt;Yet, I had to learn it the hard way that there’s no auto-merge functionality within the REST API.&lt;/p&gt;
&lt;h2 id=&quot;second-try-the-graphql-api&quot;&gt;Second try: The GraphQL API&lt;/h2&gt;
&lt;p&gt;The REST API, however, is not the only API. GitLab also has a
&lt;a href=&quot;https://docs.gitlab.com/api/graphql/&quot;&gt;GraphQL API&lt;/a&gt;. If you’re not familiar with GraphQL: It’s
another type of API specification, just in cool, because Facebook invented it, I guess. In the end,
we’re still executing HTTP requests.&lt;/p&gt;
&lt;p&gt;After tinkering for a while with &lt;a href=&quot;https://gitlab.com/-/graphql-explorer&quot;&gt;the GraphQL explorer&lt;/a&gt;
(which is admittely one of the cooler things that GraphQL allows), I ended up with the following
result:&lt;/p&gt;
&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# This can be tested here: https://gitlab.com/-/graphql-explorer&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;mutation&lt;/span&gt; &lt;span class=&quot;token definition-mutation function&quot;&gt;acceptMR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable variable-input&quot;&gt;$projectPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token scalar&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable variable-input&quot;&gt;$iid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token scalar&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable variable-input&quot;&gt;$sha&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token scalar&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property-query property-mutation&quot;&gt;mergeRequestAccept&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
		&lt;span class=&quot;token attr-name&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token attr-name&quot;&gt;projectPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable variable-input&quot;&gt;$projectPath&lt;/span&gt;
			&lt;span class=&quot;token attr-name&quot;&gt;iid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable variable-input&quot;&gt;$iid&lt;/span&gt;
			&lt;span class=&quot;token attr-name&quot;&gt;sha&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable variable-input&quot;&gt;$sha&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;# This is the important thing you&#39;ll have to adjust to your project-specific settings!&lt;/span&gt;
			&lt;span class=&quot;token attr-name&quot;&gt;strategy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS&lt;/span&gt;
			&lt;span class=&quot;token attr-name&quot;&gt;shouldRemoveSourceBranch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token property&quot;&gt;errors&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The value for the strategy is important here! Since we use Merge Trains at work, I set it to the
&lt;code&gt;ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS&lt;/code&gt; strategy. As of GitLab 17.9, the following strategies are
available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MERGE_TRAIN&lt;/code&gt;: Use the merge_train merge strategy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS&lt;/code&gt;: Use the add_to_merge_train_when_pipeline_succeeds
merge strategy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS&lt;/code&gt;: Use the add_to_merge_train_when_checks_pass merge strategy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MERGE_WHEN_CHECKS_PASS&lt;/code&gt;: Use the merge_when_checks_pass merge strategy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MERGE_WHEN_PIPELINE_SUCCEEDS&lt;/code&gt;: Use the merge_when_pipeline_succeeds merge strategy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use the strategy that fits most to your project. Place this file in &lt;code&gt;.gitlab/auto-merge.graphql&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;using-the-graphql-mutation-in-a-pipeline&quot;&gt;Using the GraphQL mutation in a pipeline&lt;/h3&gt;
&lt;p&gt;To use this &lt;em&gt;mutation query&lt;/em&gt; in a CI pipeline now, we have to add a step to the &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;.
The following snippet for your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; will do:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;enable_auto_merge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deploy
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Copied from: https://docs.gitlab.com/ee/ci/yaml/workflow.html#skip-pipelines-for-draft-merge-requests&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Disable this job for drafts. Not necessary, but it reduces the amount of jobs.&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        $CI_PIPELINE_SOURCE == &quot;merge_request_event&quot; &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; $CI_MERGE_REQUEST_TITLE =~
        /^(&#92;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Draft&#92;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&#92;(Draft&#92;)&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;Draft&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;)/
      &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; never

    &lt;span class=&quot;token comment&quot;&gt;# Enable it for all other MRs.&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;

    &lt;span class=&quot;token comment&quot;&gt;# And ignore it if we&#39;re not in a MR scope. Explicitly mentioned here for clarity.&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; never
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; alpine&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.21&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; apk add &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;no&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cache curl jq
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Until https://gitlab.com/groups/gitlab-org/-/epics/3559 is not implemented, we cannot use the&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# job token to do this. Therefore, we have to use a project token for that.&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;#&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Because the &quot;merge_when_pipeline_succeeds&quot; parameter via the REST API fails,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# we use the GraphQL API and its mergeRequestAccept mutation.&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;#&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# The `AUTOMERGE_BOT_TOKEN` is defined in the CI/CD variables of this project.&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      jq -n --rawfile query .gitlab/auto-merge.graphql &#39;{
        projectPath: env.CI_PROJECT_PATH,
        iid: env.CI_MERGE_REQUEST_IID,
        sha: env.CI_MERGE_REQUEST_SOURCE_BRANCH_SHA
      } | {variables: ., query: $query}&#39; | curl --header &quot;PRIVATE-TOKEN: $AUTOMERGE_BOT_TOKEN&quot;
      --json @- $CI_API_GRAPHQL_URL&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, it’s requiring a dedicated token for this. Any token with the &lt;code&gt;api&lt;/code&gt; scope will do. I
decided to use a &lt;a href=&quot;https://docs.gitlab.com/user/project/settings/project_access_tokens/&quot;&gt;project access token&lt;/a&gt; for this and persisted it within the &lt;code&gt;AUTOMERGE_BOT_TOKEN&lt;/code&gt;
CI/CD variable.&lt;/p&gt;
&lt;h2 id=&quot;has-it-helped-us&quot;&gt;Has it helped us?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;A lot.&lt;/strong&gt; The time to merge is effectively zero. Once the merge request is approved, it’ll get
merged automatically. Over the last couple of weeks, this increased the velocity a lot and allowed
for a much more asynchronous work.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Low-effort self-hosting using Podman</title>
    <link href="https://jasminchen.dev/notes/2025/low-effort-self-hosting-using-podman/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>3ff4d1434681ba1d281764d2e6d12352dbcd646e</id>
    <content type="html">&lt;p&gt;Because every one in the homelab community is hosting their services differently, I thought I write
about &lt;strong&gt;my&lt;/strong&gt; way of hosting containers.&lt;/p&gt;
&lt;h2 id=&quot;server-setup&quot;&gt;Server setup&lt;/h2&gt;
&lt;p&gt;My primary server is a &lt;em&gt;Lenovo ThinkCentre thin client&lt;/em&gt; (not sure about the exact model), with an
Intel Core i5-6500 and 8GB of RAM. This might not sound much first, but this machine has proven
itself to be quite capable for self-hosting.&lt;/p&gt;
&lt;p&gt;Right now I’m hosting (in alphabetical order):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/philippe44/AirConnect&quot;&gt;AirConnect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dmunozv04/iSponsorBlockTV&quot;&gt;iSponsorBlockTV&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jlesage/docker-handbrake&quot;&gt;Handbrake&lt;/a&gt; (used on-demand for pre-transcoding
videos)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.home-assistant.io/&quot;&gt;Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jellyfin.org&quot;&gt;Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.linuxserver.io/general/swag/&quot;&gt;nginx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.paperless-ngx.com/&quot;&gt;paperless-ngx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zigbee2mqtt.io/&quot;&gt;Zigbee2MQTT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;…and several other smaller webservices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As the operating system, I am using &lt;a href=&quot;https://fedoraproject.org/&quot;&gt;Fedora Server&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;why-fedora-and-not-a-debian-based-distribution&quot;&gt;Why Fedora, and not a Debian-based distribution?&lt;/h2&gt;
&lt;p&gt;I chose Fedora for my server because &lt;em&gt;personally&lt;/em&gt; I was searching for a distribution that fulfills
the following requirements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It should work out of the box.&lt;/li&gt;
&lt;li&gt;It has a community and sufficient documentation for common tasks.&lt;/li&gt;
&lt;li&gt;It provides up-to-date software without sacrificing stability or comfort.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fedora provides all of this. I was considering Arch Linux first, but even with the &lt;code&gt;archinstall&lt;/code&gt;
helper utility that’s assisting with the setup of a new Arch setup, I wasn’t motivated enough to use
it. You know, sometimes I just want things to work.&lt;/p&gt;
&lt;p&gt;And there’s an additional benefit of using Fedora: By using an operating system that has overlap
with the &lt;em&gt;Red Hat&lt;/em&gt; folks, &lt;code&gt;podman&lt;/code&gt; is available out of the box in an up-to-date version. Which means
less maintenance work, as no additional package repository is needed.&lt;/p&gt;
&lt;h2 id=&quot;why-podman-instead-of-docker&quot;&gt;Why Podman instead of Docker?&lt;/h2&gt;
&lt;p&gt;There are a couple of reasons for it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;It’s in the upstream repositories.&lt;/strong&gt; &lt;code&gt;dnf install podman&lt;/code&gt; is sufficient to run containers. No
additonal package repository required.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;It’s daemonless.&lt;/strong&gt; Docker has it’s socket running on &lt;code&gt;/var/lib/docker.sock&lt;/code&gt; running all the
time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rootless containers.&lt;/strong&gt; Podman, in comparison to Docker, has a pretty good support for running
unprileged containers. I hope that it changed already in Docker, but the last time I checked it,
running &lt;code&gt;docker&lt;/code&gt; as a regular user effectively meant sudo permissions on the machine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;It is respecting my network policies, especially the firewall.&lt;/strong&gt; When creating a container
using docker with published ports, it works by creating additional &lt;code&gt;iptables&lt;/code&gt; rules, effectively
circumventing the firewall.&lt;/p&gt;
&lt;p&gt;Which means, if I’d run &lt;code&gt;docker run -p 8080:8080 my-fancy-service:latest&lt;/code&gt;, it would be available
to the whole web, even if I’d configure the firewall to block this port. To workaround this,
you’d have to bind it to localhost only (&lt;code&gt;docker run -p 127.0.0.1:8080:8080&lt;/code&gt;), which is –
as far as I can see – almost exclusively never done in any READMEs or tutorials out there.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;podman&lt;/code&gt;, on the other hand, is less surprising and does not do such shenanigans.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tight integration with systemd.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;quadlets-aka-containers-managed-by-systemd&quot;&gt;Quadlets aka containers managed by systemd&lt;/h2&gt;
&lt;p&gt;The tight integration of systemd, especially with the availability of a unit generator out of the
box is the most undervalued feature of Podman in comparison to Docker, I think. Docker is known for
requiring &lt;code&gt;docker compose&lt;/code&gt; for basically everything that exceeds the default usage of containers
that are executed just one time using &lt;code&gt;docker run&lt;/code&gt;. The
&lt;a href=&quot;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html&quot;&gt;systemd unit generation&lt;/a&gt; of
Podman can be seen as the answer to that. It is translating &lt;code&gt;.container&lt;/code&gt; specification files to a
&lt;code&gt;podman run&lt;/code&gt; command. By following the architecture of systemd, we automatically gain a fully
functioning systemd unit as a gift.&lt;/p&gt;
&lt;p&gt;Personally, I just love this. I write my &lt;code&gt;jellyfin.container&lt;/code&gt; and automatically can manage the
Jellyfin instance using &lt;code&gt;systemctl&lt;/code&gt;. I can use &lt;code&gt;systemctl stop jellyfin&lt;/code&gt; to stop it, I can use
&lt;code&gt;journalctl --unit jellyfin&lt;/code&gt; to see its logs, it no longer feels like it is isolated from the rest
of the system.&lt;/p&gt;
&lt;h2 id=&quot;how-does-that-container-file-look-like-in-practice&quot;&gt;How does that &lt;code&gt;.container&lt;/code&gt; file look like in practice?&lt;/h2&gt;
&lt;p&gt;I’ll take Jellyfin as an example here, because it’s one of those applications that are &lt;em&gt;relatively&lt;/em&gt;
complex in their setup. My Jellyfin container specification, located in
&lt;code&gt;/etc/containers/systemd/jellyfin.container&lt;/code&gt; can be seen below.&lt;/p&gt;
&lt;p&gt;For this blog post, I added additional comments to make it easier to understand.&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# The [Container] section is read by Podman, therefore all of the following&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# parts are interpreted by Podman, not by systemd.&lt;/span&gt;
&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Using `:latest` here allows me to update Jellyfin by pulling a newer image&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# and restarting the container. The whole procedure is explained in a later section.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;ghcr.io/jellyfin/jellyfin:latest&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Required for hardware acceleration. I&#39;m passing the built-in GPU to Jellyfin here.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# https://jellyfin.org/docs/general/administration/hardware-acceleration/intel&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;AddDevice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/dev/dri/renderD128:/dev/dri/renderD128&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;GroupAdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;105&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Networks are explained later in this blog post.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Network&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;services.network&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Network&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;external.network&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;HostName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;jellyfin&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# The CONFIGURATION_DIRECTORY environment variable is set by systemd automatically,&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# when using the `ConfigurationDirectory` directive in the [Service] section.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# This effectively creates a folder /etc/jellyfin automatically, which is then mounted&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# into the container. The `:Z` at the end is required for SELinux, enabled by default&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# on Fedora.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Volume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;${CONFIGURATION_DIRECTORY}:/config:Z&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Same as above, just with /var/log/jellyfin.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Volume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;${LOGS_DIRECTORY}:/config/logs/:Z&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Volume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;${LOGS_DIRECTORY}:/config/log/:Z&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# And also /var/cache/jellyfin. By using the systemd-integration, we can clean&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# the cache later by running `systemctl clean jellyfin`.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Volume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;${CACHE_DIRECTORY}/cache:/cache:Z&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# I first used RuntimeDirectory, but the ramdisk was too small to hold the&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# on-the-fly transcodes. Therefore I moved it to the cache later.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Volume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;${CACHE_DIRECTORY}/transcodes:/config/transcodes/:Z&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# The NFS mount to my media library.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Volume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/media:/media&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# [Unit] and [Service] directives are passed as is to the generated systemd unit.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# There&#39;s nothing spectacular in here.&lt;/span&gt;
&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;Jellyfin media server&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;RequiresMountsFor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/media/music&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;RequiresMountsFor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/media/video&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;RequiresMountsFor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/media/books&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;CacheDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;jellyfin&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;LogsDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;jellyfin&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ConfigurationDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;jellyfin&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Create subfolders required for binding the folders&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStartPre&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;mkdir -p ${CACHE_DIRECTORY}/transcodes&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStartPre&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;mkdir -p ${CACHE_DIRECTORY}/cache&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Inform systemd of additional exit status. Copied as-is from the Jellyfin&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# documentation.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;SuccessExitStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;0 143&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;explaining-the-networks&quot;&gt;Explaining the networks&lt;/h3&gt;
&lt;p&gt;For my setup, I use two networks. The primary one, &lt;code&gt;services&lt;/code&gt;, is a &lt;em&gt;bridge&lt;/em&gt; network. It does not
have any special configuration values and is located in &lt;code&gt;/etc/containers/systemd/services.network&lt;/code&gt;.
Almost all containers do land in here.&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Network&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;NetworkName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;services&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Driver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;bridge&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second network, &lt;code&gt;external&lt;/code&gt; is only used in exceptions, mainly by Jellyfin and Home Assistant.
It’s a &lt;em&gt;MACVLAN&lt;/em&gt; network, which assigns those containers individual MAC adresses and makes them
appear as individual devices within the network. Jellyfin benefits from this for DHCP, Home
Assistant for the auto discovery feature.&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Network&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;NetworkName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;external&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# macvlan networks have to be bound to an interface. The default interface on my&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# host is named eno1, which is passed as an additional argument here.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;PodmanArgs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;--interface-name=eno1&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Driver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;macvlan&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Since 2024-10-26, the subnet is predefined. By doing so, we do not need DHCP&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# leases and therefore can avoid the weird router bugs.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# This shouldn&#39;t be necessary usually, only with very very weird routers.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Subnet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;192.168.188.0/24&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Gateway&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;192.168.188.1&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;IPRange&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;192.168.188.220-192.168.188.250&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing that can be noticed from the above Jellyfin configuration: none of the ports are exposed.
In my setup, &lt;strong&gt;they don’t have to&lt;/strong&gt;. What’s set, is the hostname (&lt;code&gt;HostName=&lt;/code&gt;), hardcoded to
&lt;code&gt;jellyfin&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;external-access-using-nginx-as-the-reverse-proxy&quot;&gt;External access using nginx as the reverse proxy&lt;/h3&gt;
&lt;p&gt;To reach my services, I am using the &lt;a href=&quot;https://docs.linuxserver.io/general/swag&quot;&gt;linuxserver.io/swag&lt;/a&gt;
container. It is a nginx reverse proxy, together with automatic ACME support out of the box. I use
nginx, because I am familiar with it, but you’re not bound to this decision as well. If you want,
you can use any reverse proxy out of the box.&lt;/p&gt;
&lt;p&gt;My nginx container is the only container that publishes ports for HTTP and HTTPS communication.&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token key attr-name&quot;&gt;Network&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;services.network&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;PublishPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;443:443/tcp&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;PublishPort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;80:80/tcp&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because nginx is also connected to the &lt;code&gt;services&lt;/code&gt; network, it can resolve all the other containers
by it’s hostname using the internal DNS of Podman.&lt;/p&gt;
&lt;p&gt;To keep this blog post focused, I won’t explain further details of my reverse proxying configuration
here.&lt;/p&gt;
&lt;h3 id=&quot;applying-defaults-to-all-containers&quot;&gt;Applying defaults to all containers&lt;/h3&gt;
&lt;p&gt;The Jellyfin configuration above is lacking several features, including but not limited to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;automatic updates&lt;/li&gt;
&lt;li&gt;automatic start on boot&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I am implementing those by using drop-ins. Drop-ins are smaller configuration files that apply to
&lt;strong&gt;all&lt;/strong&gt; of my containers. I love this feature a lot, because it allows me to enforce a common set of
rules for my containers. Those drop-in files are located in &lt;code&gt;/etc/containers/systemd/container.d&lt;/code&gt;.
For example, I have a &lt;code&gt;01-registry-auto-update.conf&lt;/code&gt; with the contents of:&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Enables automatic updates for all containers. This is done via the&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# podman-autoupdate systemd timer.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;AutoUpdate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;registry&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures that all of my containers automatically update once a day (as defined in
&lt;code&gt;podman-auto-update.timer&lt;/code&gt;). The automatic update mechanism is pretty simple in comparison to tools
like Watchtower, but it served me for a year now without any problems. I can only recommend it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;02-enable-automatic-startup.conf&lt;/code&gt; ensures that all my containers start when booting the machine.&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Install&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;default.target&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another file, &lt;code&gt;03-restart-on-failure.conf&lt;/code&gt;, defined below, automatically restarts my containers.
Another useful thing that you definitely don’t wanna miss out.&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Automatically restarts the container if it exists with a non-zero exit code.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;on-failure&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;starting-the-container&quot;&gt;Starting the container&lt;/h3&gt;
&lt;p&gt;After the container is configured, I reload the systemd daemon (&lt;code&gt;systemctl daemon-reload&lt;/code&gt;) and start
the service (&lt;code&gt;systemctl start jellyfin&lt;/code&gt;). They’re enabled automatically due to the &lt;code&gt;[Install]&lt;/code&gt;
directive.&lt;/p&gt;
&lt;p&gt;That’s it! Jellyfin is now running and I can watch my videos. :3&lt;/p&gt;
&lt;h2 id=&quot;how-does-the-generated-unit-file-look-like&quot;&gt;How does the generated unit file look like?&lt;/h2&gt;
&lt;p&gt;The systemd generator in &lt;code&gt;/usr/lib/systemd/system-generators/podman-system-generator&lt;/code&gt; generates
systemd units that are placed in &lt;code&gt;/run/systemd/generator/&lt;/code&gt;. The important part is this:&lt;/p&gt;
&lt;pre class=&quot;language-systemd&quot;&gt;&lt;code class=&quot;language-systemd&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# /run/systemd/generator/jellyfin.service&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Automatically generated by /usr/lib/systemd/system-generators/podman-system-generator&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;PODMAN_SYSTEMD_UNIT=%n&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;KillMode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;mixed&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStartPre&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;mkdir -p ${CACHE_DIRECTORY}/transcodes&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStartPre&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;mkdir -p ${CACHE_DIRECTORY}/cache&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/usr/bin/podman run --name systemd-%N --cidfile=%t/%N.cid --replace --rm --cgroups=split --hostname jellyfin --group-add 105 --network services --network external --sdnotify=conmon -d --device /dev/dri/renderD128:/dev/dri/renderD128 -v ${CONFIGURATION_DIRECTORY}:/config:Z -v ${LOGS_DIRECTORY}:/config/logs/:Z -v ${LOGS_DIRECTORY}:/config/log/:Z -v ${CACHE_DIRECTORY}/cache:/cache:Z -v ${CACHE_DIRECTORY}/transcodes:/config/transcodes/:Z -v /media:/media --label io.containers.autoupdate=registry ghcr.io/jellyfin/jellyfin:latest&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/usr/bin/podman rm -v -f -i --cidfile=%t/%N.cid&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStopPost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;-/usr/bin/podman rm -v -f -i --cidfile=%t/%N.cid&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Delegate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;&lt;span class=&quot;token boolean&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;notify&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;NotifyAccess&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;all&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;SyslogIdentifier&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;%N&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, in the end it’s just a &lt;code&gt;podman run&lt;/code&gt; invocation. If there’s something wrong with it,
it’s pretty easy to debug it, either by changing the command or by adding additional commands ahead
of the podman execution, for example to set a value for &lt;code&gt;sysctl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That allows you to run a common &lt;code&gt;setup.sh&lt;/code&gt; or something else without resorting to Makefiles or
custom bash scripts that run &lt;code&gt;docker compose&lt;/code&gt; in the end!&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;Without additional system dependencies (apart from &lt;code&gt;podman&lt;/code&gt;), I now have a container that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;automatically booting on startup&lt;/li&gt;
&lt;li&gt;updating itself every day&lt;/li&gt;
&lt;li&gt;not exposed to the public internet, but behind my reverse proxy&lt;/li&gt;
&lt;li&gt;managed via systemd, therefore automatically picked up by any monitoring solutions that monitor it&lt;/li&gt;
&lt;li&gt;declaratively managed&lt;/li&gt;
&lt;li&gt;following a consistent pattern of configuration paths just like non-containerised services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And, most importantly: &lt;strong&gt;Once they run, I don’t have to touch them anymore&lt;/strong&gt; (unless they break
because of an update).&lt;/p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;
&lt;p&gt;If you wanna start with that setup as well, I recommend you to check out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the
&lt;a href=&quot;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html&quot;&gt;podman-systemd.unit configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;this &lt;a href=&quot;https://codeberg.org/herzenschein/herz-quadlet&quot;&gt;repository of quadlet units&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git.sr.ht/~nachtjasmin/quadlet-files&quot;&gt;my hand-picked collection of quadlet files&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, feel free to ask me any questions about this setup, either by mail or on the fediverse. 😊&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Sorting series in Jellyfin by the DVD order</title>
    <link href="https://jasminchen.dev/notes/2025/sorting-series-in-jellyfin-by-the-dvd-order/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>88acba4f0d525f64785acedfe2f35d4945078216</id>
    <content type="html">&lt;p&gt;Today I learned that it’s quite common for TV series to be aired in a non-chronological order. This
can make the media management a bit more annoying, since automatic tagging systems might take the
airing date as the ordering.&lt;/p&gt;
&lt;p&gt;This also happened to me with
&lt;a href=&quot;https://www.themoviedb.org/tv/30991-cowboy-bebop/episode_group/606a5cf909c24c00782bbf59/group/606a5d4909c24c0040c8a5fe&quot;&gt;Cowboy Bebop&lt;/a&gt;,
a series where the “first” episode was actually the 13th. When opening the TMDB, I saw like there’s
not just one way to order the episode, but a mere six of them!&lt;/p&gt;
&lt;p&gt;Anyway, I wanted the correct order in Jellyfin instance. Fortunately, I found
&lt;a href=&quot;https://forum.jellyfin.org/t-resolved-dvd-order-instead-of-aired-order&quot;&gt;a solution in the forum&lt;/a&gt;
for this exact problem:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Edit the metadata of the show (not just the season).&lt;/li&gt;
&lt;li&gt;Scroll down to the “Display order” and change it. In the case of &lt;em&gt;Cowboy Bebop&lt;/em&gt;, I changed it to
“DVD”.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://jasminchen.dev/notes/2025/sorting-series-in-jellyfin-by-the-dvd-order/wgyo4XulEV-882.avif 882w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://jasminchen.dev/notes/2025/sorting-series-in-jellyfin-by-the-dvd-order/wgyo4XulEV-882.webp 882w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://jasminchen.dev/notes/2025/sorting-series-in-jellyfin-by-the-dvd-order/wgyo4XulEV-882.png&quot; alt=&quot;The field in the Jellyfin user interface, marked with a red rectangle.&quot; width=&quot;882&quot; height=&quot;314&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;In my case, I also had to refetch all the metadata and replace it in order to take effect, but it
&lt;em&gt;might&lt;/em&gt; work without that step in your case. After that, the series is in the correct order with no
manual change on the metadata being necessary.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Cleaning up my home directory on a schedule with systemd-tmpfiles</title>
    <link href="https://jasminchen.dev/notes/2025/cleaning-up-the-downloads-folder-on-a-schedule/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>d4b769dd587e856c9c69250d8d5de4bcc4322fc6</id>
    <content type="html">&lt;p&gt;I don’t know about you, but I am using both &lt;code&gt;~/Desktop&lt;/code&gt; and &lt;code&gt;~/Downloads&lt;/code&gt; as some kind of temporary
workdir. Almost everything I work on ends up in there, from memes to Linux ISOs to archives made via
&lt;code&gt;wget&lt;/code&gt;. And over time, my &lt;code&gt;~/Downloads&lt;/code&gt; ended up to be a total mess which I randomly forgot about.&lt;/p&gt;
&lt;p&gt;Furthermore I realised that the &lt;code&gt;XDG_CACHE_DIR&lt;/code&gt; (commonly located in &lt;code&gt;~/.cache&lt;/code&gt;) tended to fill up
with random stuff over time. I am not sure whether it’s the responsibility of the user of the
application to clean up files in there, but heck, there were a lot of them. And when I glimpsed over
it using &lt;code&gt;ncdu&lt;/code&gt;, I realised that most of it hasn’t been touched for a long time.&lt;/p&gt;
&lt;p&gt;Therefore I needed a plan to clean up those folder. I first thought about writing a simple bash
script that checks the age of my files and folders and is executed on boot. But then I remembered
&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd-tmpfiles.html&quot;&gt;&lt;code&gt;systemd-tmpfiles&lt;/code&gt;&lt;/a&gt;
and it’s support for user-specific configuration entries in &lt;code&gt;~/.config/user-tmpfiles.d&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And that’s how the configuration below was born:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#Type Path                                     Mode User Group Age         Argument

# Delete all downloads older than one month
d %h/Downloads/ - - - 30d

# Delete cache files older than a month
d %h/.cache/ - - - 30d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Although presented in a single block here, I split them up across two files, &lt;code&gt;downloads.conf&lt;/code&gt; and
&lt;code&gt;local-cache.conf&lt;/code&gt; respectively. &lt;code&gt;systemd-tmpfiles&lt;/code&gt;
&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/tmpfiles.d.html&quot;&gt;provides a lot of useful parameters&lt;/a&gt;,
but I am using the &lt;code&gt;d&lt;/code&gt; type here.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;d&lt;/code&gt;: Create a directory. The mode and ownership will be adjusted if specified. Contents of this
directory are subject to time-based cleanup if the age argument is specified.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With the configuration line above this translates to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create &lt;code&gt;~/Downloads&lt;/code&gt;, ignore existing mode, user and group and delete everything older than 30
days&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that’s exactly what I need. Now I just need to enable the respective timer
(&lt;code&gt;systemctl enable --now --user systemd-tmpfiles-clean.timer&lt;/code&gt;) and I’m done. By default, the timer
runs five minutes after boot and every day after that. So, even if I’d forgot to turn off my
devices, the files would be cleaned up properly.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>I&#39;ve just released older projects to the public domain</title>
    <link href="https://jasminchen.dev/notes/2025/public-domain-offensive/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>b8ea9164c66ffe59d7ee8722522d5a572551dcfe</id>
    <content type="html">&lt;p&gt;Because copyright licenses are a weird concept to me (and because licenses all suck to a certain
degree, at least for me), I decided to relicense some of my older repositories to the public domain.&lt;/p&gt;
&lt;p&gt;Therefore, the following repositories are now part of the public domain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nachtjasmin/dotfiles&quot;&gt;my dotfiles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git.sr.ht/~nachtjasmin/mastodon-alt-text-reminder-bot&quot;&gt;mastodon-alt-text-reminder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git.sr.ht/~nachtjasmin/bonkme&quot;&gt;bonkme&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As part of the license change, I also moved some of my repositories to Sourcehut in order to remove
my dependency on GitHub.&lt;/p&gt;
&lt;p&gt;For the future, I plan to move all repositories to the public domain before I “retire” them. It’s
not much, but if it helps at least one other person on this planet, it’s a win for public knowledge
sharing. :3&lt;/p&gt;
&lt;p&gt;If you wanna do the same, I’d encourage you to place
&lt;a href=&quot;https://choosealicense.com/licenses/unlicense/&quot;&gt;the Unlicense license&lt;/a&gt; within your repository.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Addendum: after publishing the post I realised that the first day in the new year is also referenced
as &lt;a href=&quot;https://en.wikipedia.org/wiki/Public_Domain_Day&quot;&gt;Public Domain Day&lt;/a&gt; 🙈&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The retrospect of a social media break</title>
    <link href="https://jasminchen.dev/notes/2024/recap-of-a-social-media-break/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>cb4ea10eb048c2515d8116343c9d1f57b5e20bfb</id>
    <content type="html">&lt;p&gt;Aaaand another blog post, close to the end of the year. That should go into my go-to explanation
guide “how adhd can be perceived in adults” – if I had one. Do not expect any kind of
structure, this post is going to be a stream of consciousness.&lt;/p&gt;
&lt;p&gt;If you’ve followed me on the fediverse, you are aware of the fact that I took a break. I might write
about the consequences of that as well and the thoughts I had in my absence of social media. But not
this year anymore, because, &lt;em&gt;yeah, of course&lt;/em&gt;. It’s almost 2025, I’m just waiting for ny partner in
order to build up a small fire to roast some &lt;em&gt;Stockbrot&lt;/em&gt; and marshmallows.&lt;/p&gt;
&lt;p&gt;Anyway, back to topic. One of the realisations I had in the last couple of weeks is that social
media always served as some kind of knowledge sharing to me. Whenever I stumbled across something or
was playing around with some technology, I shared that on the fediverse (or previously on Twitter).
And now, when I took a break I realised pretty quickly: I miss that knowledge posting. Not just
mine, but also from others. I missed and still miss the ability to learn from others. Taking a break
from social media has it’s advantages, but for me, this was by far the biggest disadvantage.&lt;/p&gt;
&lt;p&gt;And the more I thought about it, the more I realised that I’m not sure whether I want to use social
media in the way I did before. This is not going to be a post that says “build your own website” or
“own your content” (jeez, I hate the word content so much), it is merely a reminder that all the
knowledge you’ve shared on any of those platforms is going to be lost one day. &lt;strong&gt;And I don’t wanna
lose it, I want to keep it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are, of course, enough posts that aren’t meant for longevity (jokes and such, I think) but the
things you’ve learned about and even shared with others? I’d love to read all of them later. And
even though Mastodon as a micro-blogging platform is better than the other corporate-branded
platforms in terms of the open web, it’s still far from perfect.&lt;/p&gt;
&lt;p&gt;One example? Lennart Poettering, one of the core maintainers of systemd, started to share some
information ahead of the releases itself in forms of little toots. This can be seen e.g.
&lt;a href=&quot;https://mastodon.social/@pid_eins/112353324518585654&quot;&gt;in this thread&lt;/a&gt;, where &lt;code&gt;run0&lt;/code&gt; was explained.
Lennart also has a blog, where this just ended up as
&lt;a href=&quot;https://0pointer.net/blog/announcing-systemd-v256.html&quot;&gt;just another link to the actual thread&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I understand it to a certain extent. Writing several smaller has threads on a social media platform
has a lower friction than to write a fully featured blog post. On the other hand, I miss the ability
to link to the actual blog posts of people and their website. Social media profiles all look the
same, websites do have some variation. (Though I also remember the “old” times, where everyone was
just using the default &lt;em&gt;Twenty Something&lt;/em&gt; themes of WordPress.)&lt;/p&gt;
&lt;p&gt;And the same goes for my friends. I missed all their knowledge when I took a break and the joy to
learn something new. I really wish I could read their ideas and knowledge in a different form. And I
also regret not storing &lt;em&gt;my own&lt;/em&gt; knowledge in a different place as well. There are tools like
&lt;em&gt;Notion&lt;/em&gt;, &lt;em&gt;Obsidian&lt;/em&gt; or a plain MediaWiki installation somewhere and sometimes I wonder why we keep
it private. Why don’t we share our own wikis with the world? Wasn’t this the whole intention of the
web that Tim Berners-Lee imagined it? Why don’t we do it?&lt;/p&gt;
&lt;p&gt;(oh, almost time for the fire, now I have to rush a bit…)&lt;/p&gt;
&lt;p&gt;Especially for me, who’s living in a tech-savvy bubble, I don’t think it’s technical problem. Did we
got so bad at concentrating? As for me, with ADHD, I might answer that with &lt;em&gt;maybe&lt;/em&gt;. It is (at least
for me), pretty hard to write a longer post like this. And the reason why it’s working this time is
because of the fact that it’s purely text-based. (Working with images in a static site generator is
painful, change my mind.) I’m just in a flow and reduced the friction for me to write longer texts.&lt;/p&gt;
&lt;p&gt;But it’s not the texts I want to keep nor do I expect that everyone is capable of maintaining their
own wiki.&lt;/p&gt;
&lt;p&gt;I just have a simple wish for the future and to you, dear reader: Please share your knowledge with
the world in a format that allows for longevity. Install a wiki, start a blog, heck, even start a
forum for your community. But please, don’t have it get lost on Discord, Matrix or the fediverse.
You don’t have to hide.&lt;/p&gt;
&lt;p&gt;(And I don’t have to as well, therefore this is one of my goals for 2025 as well.)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Hello, new ThinkPad!</title>
    <link href="https://jasminchen.dev/notes/2024/think-pad-t14-gen-2/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>d4a1fa6bdab233c7c5ed61adad8b2a57da541b88</id>
    <content type="html">&lt;p&gt;After carrying my “old” ThinkPad T460 for a while (seven years!) now, it was time for a replacement.
And, well, I have to admit that it took me a while to actually buy a newer device for me, as the old
ThinkPad isn’t broken. It’s just, let’s say, not in its best conditions. The two batteries have a
combined lifetime of like 20 minutes and are so close of becoming a very fancy spicy pillow. That
was the first reason to buy a newer device.&lt;/p&gt;
&lt;p&gt;The second reason was the lack of Thunderbolt, which made it impossible to integrate the device with
my work setup. This always resulted in me sitting in the bed next to the charger. Bad for the back,
even worse for long-term posture.&lt;/p&gt;
&lt;p&gt;One day I took the initiative and lurked a bit on ebay, searching for a good deal for a newer
ThinkPad. I peeked at the Framework devices, but they’re still to expensive for me. I didn’t want to
spend more than 400 EUR in total and originally aimed at 200-300 EUR. Not much, but plenty enough
for a newer device, even refurbished.&lt;/p&gt;
&lt;p&gt;Sadly, none of the deals online looked particularly good to me. They we’re reasonable, but never
reached my “this is a no-brainer, I &lt;strong&gt;have&lt;/strong&gt; to buy this now” line. I mean, what do I want with a
newer device that only has a display resolution of 1366x768 pixels? Even if everything else is in
perfect conditions, this isn’t a good deal. At least not for me.&lt;/p&gt;
&lt;p&gt;It took me some days until I found a deal that was too good to be true. A ThinkPad T14 Gen 2 (11th
Gen Intel Core i5), which is the first generation of the Thunderbolt 4 devices! All other devices
had “only” Thunderbolt 3, that device really stood out. To my surprise, it even was within my
desired price range with a cost of ~360 EUR, which is, holy fuck, pretty pretty cheap! So, there has
to be a quirk somewhere, right? And from the description alone, my “this is a scam” radar hit pretty
quickly. The description was almost empty (it was more or less just a “ThinkPad to sell, charger
included.”) and the seller was pretty inactive as well. But hey, there are some protection
mechanisms by eBay itself and the account was a stunning 20 years old (holy fuck, eBay is a pretty
old webpage), so I just decided to give it a go and hope for the best.&lt;/p&gt;
&lt;p&gt;And that gamble paid off. I’m now the owner of a device in an almost perfect condition. Even the
battery reports a health of 100%! The luck really really paid off and hopefully, that little guy is
going to serve me another seven years. 😊&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>jujutsu made me realise how bad the UX of Git actually is</title>
    <link href="https://jasminchen.dev/notes/2024/jujutsu-a-different-approach-to-git/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>4006c3c500d01ae1f50544889765b6f876496d1e</id>
    <content type="html">&lt;p&gt;Nowadays, Git is the common tooling that every software engineer has to know about. And due to the
rise of DevOps approaches, not only developers, but operators (or “infrastructure engineers”/“DevOps
engineer”/&lt;em&gt;some other fancy term here&lt;/em&gt;) as well.&lt;/p&gt;
&lt;p&gt;And if you’re a Gen Z kid like me, Git is likely the only version-control system (VCS) that you’ve
ever learned. A GitHub account is more or less ubiquitious nowadays, which is unfortunate, I think.
Because now they’re used interchangeably and they aren’t. Git has limitations, but GitHub has even
more.&lt;/p&gt;
&lt;p&gt;Don’t get me wrong, Git is not bad per se. The decentralised approach, the ability to work offline
and its abilities to deal with merge conflicts are pretty powerful. And yet, the default user
experience of the &lt;code&gt;git&lt;/code&gt; command could be better.&lt;/p&gt;
&lt;p&gt;How much, you might ask? Well, that’s the thing I realised when I tried the new kid on the block,
&lt;code&gt;jj&lt;/code&gt;. I never tried one of the other VCS systems out there, whether it’s Mercurial, Subversion,
Fossil or the VCS of Microsoft that I forgot the name of.&lt;/p&gt;
&lt;h2 id=&quot;but-what-is-bad-about-the-user-experience-of-git&quot;&gt;But what is bad about the user experience of Git?&lt;/h2&gt;
&lt;p&gt;Git has a really, &lt;em&gt;really&lt;/em&gt; high learning curve. You have to learn about commits, branches, what the
&lt;code&gt;HEAD&lt;/code&gt; pointer is, the difference between “untracked” and “staged”, etc. That’s quite a lot for
newcomers to deal with.&lt;/p&gt;
&lt;p&gt;To continue, you have different ideas what a commit is per team/employer/ project. One team might
stick to Conventional Commits,
&lt;a href=&quot;https://jasminchen.dev/notes/2022/why-conventional-commits-are-a-bad-idea/&quot;&gt;which I already ranted about in 2022&lt;/a&gt;,
another one might enforce squashing on merge/pull requests and so on.&lt;/p&gt;
&lt;p&gt;And you have to learn all of this in order to commit the smallest changes to a project.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Yeah, of course, it’s a pretty powerful tool! It takes time to practice it!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s certainly true, yes. But: I stopped counting how many workshops/tutorials/wiki pages I wrote
for others to explain “basic” stuff of Git itself, just so that I, as a reviewer, have an easier job
to do. No matter how much practice someone has in Git, there’s always a thing that could be improved
from my point as a reviewer.&lt;/p&gt;
&lt;p&gt;The problem I faced with most of the commits I’ve seen over the last decade is: &lt;strong&gt;an unclear scope
of a commit.&lt;/strong&gt; Changes that are actually separate from the description itself (a typo here, some
code formatting there) are so common that if I’d count the “days since it last happened”, the
counter would never go above 30. &lt;strong&gt;Those “leaks” of changes are happening at least(!) once a
month.&lt;/strong&gt; And that is pretty annoying, especially as a reviewer.&lt;/p&gt;
&lt;h3 id=&quot;so-what-do-in-this-case&quot;&gt;So, what do in this case?&lt;/h3&gt;
&lt;p&gt;To be honest, I ignore most of such “change leaks” all the time, as long as it’s limited to
formatting. It annoys me, sure, but I accept it. I know that the Git user experience is bad and
therefore treat those diffs as a symptom of the tooling rather than a lack of knowledge.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-of-git-in-a-nutshell&quot;&gt;The problem of Git in a nutshell&lt;/h3&gt;
&lt;p&gt;Because the Git user experience is so frustrating to most of its users, it &lt;strong&gt;encourages you to do
less commits&lt;/strong&gt;. It is possible to just use &lt;code&gt;git commit -am &amp;quot;wip&amp;quot;&lt;/code&gt; or do some combination of
&lt;code&gt;git commit --fixup&lt;/code&gt; together with interactive rebases. And this is working pretty nicely, as long
as your changes are limited in their scope. Except: that’s almost never the case.&lt;/p&gt;
&lt;p&gt;I cannot speak for the majority of developers out there, but I &lt;strong&gt;never&lt;/strong&gt; work on just feature, it’s
always at least three or even more. And those features/changes are not mutually exclusive to each
other. Sometimes I have fixed something in one branch (even if it’s just a &lt;code&gt;npm run format-all&lt;/code&gt;),
which then has to be repeated on the next branch again.&lt;/p&gt;
&lt;p&gt;Not to mention the fact that at least I don’t know always what I’m working on next. I just open the
repository locally and start doing something. Not every change has a proper bug ticket or a clear
scope, sometimes it’s just refactorings all over the place. Even when I’m working on a dedicated
bug: No codebase is perfect, there’s &lt;em&gt;always&lt;/em&gt; something that I see and work on.&lt;/p&gt;
&lt;p&gt;And yes, I can interrupt my current edits and create a small intermediate commit using &lt;code&gt;git add -p&lt;/code&gt;,
but even then: It’s on the same branch as the feature I’m working on, so I have to get it out of my
branch history. As soon as you’re doing this multiple times a day or a week, you’ll get annoyed by
it.&lt;/p&gt;
&lt;h2 id=&quot;an-alternative-approach-stacked-diffs&quot;&gt;An alternative approach: stacked diffs&lt;/h2&gt;
&lt;p&gt;I won’t go into detail here, but the idea of stacked diffs does not rely on branches at all.
Instead, each commit is an independent and &lt;em&gt;theoretically&lt;/em&gt; isolated change. Instead of having one
branch &lt;code&gt;feature-1&lt;/code&gt;, which contains commits &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt; and &lt;code&gt;C&lt;/code&gt;, you’re working on something that’s
commonly called a &lt;em&gt;changeset&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This changeset has a unique identifier and evolves over time, but it always ends up in one commit.
Tools working with changesets, for example &lt;a href=&quot;https://www.gerritcodereview.com/&quot;&gt;Gerrit&lt;/a&gt;, allow reviewers to see how a change evolved
over time.&lt;/p&gt;
&lt;p&gt;It looks similar on a first glance, but it fundamentally changes the way one as a developer and
reviewer alike is working with repositories. It encourages developers to keep their individual
commits clean and focused. And what’s more important: you aren’t no longer working with individual
branches, &lt;strong&gt;you can always commit straight to &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That’s just my idea of how I approach them, there’s a more
&lt;a href=&quot;https://jg.gg/2018/09/29/stacked-diffs-versus-pull-requests/&quot;&gt;detailed explanation for stacked diffs by Jackson Gabbard&lt;/a&gt;,
which I can only recommend for reading.&lt;/p&gt;
&lt;h2 id=&quot;my-experiences-with-jujutsu&quot;&gt;My experiences with jujutsu&lt;/h2&gt;
&lt;p&gt;But let’s head back to the introduction of this post and the little tool mentioned in the title,
&lt;a href=&quot;https://github.com/jj-vcs/jj&quot;&gt;&lt;code&gt;jujutsu&lt;/code&gt;&lt;/a&gt;, or short, &lt;code&gt;jj&lt;/code&gt;. I found out about it back in May 2024, according to &lt;a href=&quot;https://github.com/nachtjasmin/dotfiles/commit/5b4544d&quot;&gt;this commit from
my dotfiles&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;jujutsu is a &lt;em&gt;relatively&lt;/em&gt; new version-control system (VCS), which describes itself as “both simple
and powerful”. And even though I was very skeptical when I read it first, I tried it out.&lt;/p&gt;
&lt;h3 id=&quot;adoption-is-easy&quot;&gt;Adoption is easy&lt;/h3&gt;
&lt;p&gt;Since jj is using Git under the hood, I was able to try it out immediately on any code repository
that I have checked out locally. This is a massive benefit, as it requires no changes to existing
infrastructure. I don’t have to convince my team to use it, I don’t have to introduce new tooling to
add server-side support. Especially in a business context where everything needs some kind of
approval or discussion beforehand, this is a &lt;strong&gt;massive&lt;/strong&gt; benefit.&lt;/p&gt;
&lt;h3 id=&quot;the-lack-of-a-staging-area-is-actually-nice&quot;&gt;The lack of a staging area is actually nice&lt;/h3&gt;
&lt;p&gt;jj reduces the “areas” from Git to just two: either the file is untracked or it’s tracked &lt;strong&gt;and&lt;/strong&gt;
committed. There’s no manually managed staging area. The current state is committed as soon as you
execute &lt;code&gt;jj&lt;/code&gt;. That’s always the case, unless the &lt;code&gt;--ignore-working-copy&lt;/code&gt; is given. As a consequence,
executing &lt;code&gt;git add&lt;/code&gt; is no longer required as it’s done for you.&lt;/p&gt;
&lt;h3 id=&quot;branches-are-no-longer-used-for-communication&quot;&gt;Branches are no longer used for communication&lt;/h3&gt;
&lt;p&gt;Because branches are only used for pushing changes to review, they don’t have to be named &lt;strong&gt;ahead&lt;/strong&gt;
of the change itself. At first I still created the branches manually in order to follow the naming
guide within the team. But it turned out to be a burden that’s almost always unnecessary, because:
jj has commands to simplify pushing branches with the name being autogenerated!&lt;/p&gt;
&lt;p&gt;It’s the change ID (which doesn’t change even when committing stuff) and therefore, together with my
user-configured prefix, all my branches are named seemingly “random”, even though the name carries
an actual meaning. My branches are now looking like: &lt;code&gt;joster/change-adjslkangk1da&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The usual reactions at work were:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;what the heck even is this branch name?&lt;/li&gt;
&lt;li&gt;did you just keysmash?&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;And of course, if you’re used to branches like &lt;code&gt;JIRA-123/add-new-fancy-feature&lt;/code&gt; or
&lt;code&gt;refactoring-of-user-service&lt;/code&gt;, those branches do seem random. In retrospect however, I do not miss
manual branch names. For the case I have to use them anyway, I can do it with a simple
&lt;code&gt;jj bookmark set refactoring-of-user-service -r adjs&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;git-lfs-is-still-missing&quot;&gt;git-lfs is still missing&lt;/h3&gt;
&lt;p&gt;I think this is one of &lt;em&gt;the&lt;/em&gt; features that I’m still missing from time to time. I’ve used git-lfs in
the past and on repositories that use it, I cannot use jj as I’d love to. The
&lt;a href=&quot;https://github.com/jj-vcs/jj/issues/80&quot;&gt;open issue regarding git-lfs support&lt;/a&gt; provides a lot of
interesting insights about the feature itself and although it’s still a bummer for me, I can live
with the current situation as is. Especially because I know that once the feature is going to land,
it’ll likely be much more polished than the native behaviour anyway.&lt;/p&gt;
&lt;h3 id=&quot;the-ability-to-work-on-multiple-things-at-the-same-time-is-astonishing&quot;&gt;The ability to work on multiple things at the same time is astonishing&lt;/h3&gt;
&lt;p&gt;It is absolutely incredible how, yeah, almost perfect the user experience of jj is. When working on
any project now, my workflow is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;open the repository&lt;/li&gt;
&lt;li&gt;start working in the codebase&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As soon as I think I’m done, I check the diff (&lt;code&gt;jj diff&lt;/code&gt;) and if the scope is already clear, I just
&lt;code&gt;jj desc&lt;/code&gt;-ribe and push it (&lt;code&gt;jj git push -c @&lt;/code&gt; (&lt;code&gt;@&lt;/code&gt; is the current revision)). If I have to split
the changes up, I use &lt;code&gt;jj split&lt;/code&gt; (the naming is so clear, it’s wonderful), which opens an editor
where I can select what’s going into each of the patch. This might be repeated multiple times for as
often as required and that’s it. Rebasing, merging, etc. — that’s all done automatically. And
if I’m not happy with the current commit log, I can rebase the changes and easily place my commits
whereever I want them to be.&lt;/p&gt;
&lt;h3 id=&quot;dealing-with-conflicts-got-so-much-easier&quot;&gt;Dealing with conflicts got so much easier&lt;/h3&gt;
&lt;p&gt;Previously, when working with Git, merge conflicts were always a pain. They always had to be
resolved &lt;strong&gt;immediately&lt;/strong&gt;. With jj however, the conflicting commits are just marked as “conflicted”
and do not need to be resolved now, they can be resolved at any time later. Dealing with conflicts
is becoming just another commit, which then gets squashed into the conflicting one.&lt;/p&gt;
&lt;h3 id=&quot;jj-has-a-pretty-low-learning-curve&quot;&gt;jj has a pretty low learning curve!&lt;/h3&gt;
&lt;p&gt;To be quite honest, I had my doubts when switching to jujutsu. Learning new tools when you already
have reliable ones working, is something that needs to be carefully considered. Especially the new
syntaxes for revision sets and templates were bugging me off at first. But in the end, they were
actually pretty easy to understand and so, &lt;em&gt;so&lt;/em&gt; incredibly powerful that I don’t know how I’d work
without them today.&lt;/p&gt;
&lt;h3 id=&quot;also-missing-pre-commit-hooks&quot;&gt;Also missing: pre-commit hooks&lt;/h3&gt;
&lt;p&gt;pre-commit hooks are still fascinating to me. Not because the idea behind is magic, but because how
many devs seem to use it. Meanwhile I always found every millisecond blocking me from committing
changes rather annoying and prefer to do the code formatting and such manually.&lt;/p&gt;
&lt;p&gt;Therefore, the lack of pre-commit hooks are not a blocker for me, but I wanted to mention them here
anyway for completion.&lt;/p&gt;
&lt;h3 id=&quot;bare-jj-isn-t-quite-as-useful-but-extremely-powerful-after-configuration&quot;&gt;“bare” jj isn’t quite as useful, but extremely powerful after configuration&lt;/h3&gt;
&lt;p&gt;jj has a powerful syntax for filtering revisions, called
&lt;a href=&quot;https://jj-vcs.github.io/jj/latest/revsets/&quot;&gt;“revsets”&lt;/a&gt;. It allowed me to configure jj to my
likings. For example, &lt;code&gt;jj open&lt;/code&gt; shows all of my unfinished changes and with &lt;code&gt;jj remain ready&lt;/code&gt; I can
rebase all ready (read: all open, but without the unfinished ones) revisions automatically on top of
the current &lt;code&gt;main&lt;/code&gt; branch. Yes, &lt;strong&gt;all of them&lt;/strong&gt;. This is like &lt;code&gt;git rebase&lt;/code&gt;, but on steroids!&lt;/p&gt;
&lt;h2 id=&quot;recap-i-don-t-miss-git&quot;&gt;Recap: I don’t miss &lt;code&gt;git&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;It took me maybe a week or two to understand jj. But it is so powerful that I can use it for ~95% of
all of my work in repositories. It even is so good that I stopped installing &lt;code&gt;git&lt;/code&gt; on new machines
altogether. That’s huge.&lt;/p&gt;
&lt;p&gt;Martin and all the developers have my deepest respect for creating such a wonderful piece of
software.&lt;/p&gt;
&lt;p&gt;And maybe, you want to try it out as well now. In this case,
&lt;a href=&quot;https://jj-vcs.github.io/&quot;&gt;head to the project page&lt;/a&gt; and start exploring it.&lt;/p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Jackson Gabbard,
&lt;a href=&quot;https://jg.gg/2018/09/29/stacked-diffs-versus-pull-requests/&quot;&gt;Stacked Diffs Versus Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Arne Bahlo, &lt;a href=&quot;https://arne.me/blog/jj-in-practice&quot;&gt;jujutsu in practice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sandy Maguire, &lt;a href=&quot;https://reasonablypolymorphic.com/blog/jj-strategy/index.html&quot;&gt;Jujutsu strategies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Benjamin Tan, &lt;a href=&quot;https://ofcr.se/jujutsu-merge-workflow&quot;&gt;A Better Merge Workflow with Jujutsu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>jasmin wrapped</title>
    <link href="https://jasminchen.dev/notes/2024/jasmin-wrapped/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>286ef9a288d055cb6b133d76b517a241a6650940</id>
    <content type="html">&lt;p&gt;The holiday season is near, therefore it’s time for me to revisit what I’ve done (fuck, now &lt;em&gt;Linkin
Park&lt;/em&gt; is playing in my head) in the last year. Instead of answering questions
&lt;a href=&quot;https://jasminchen.dev/notes/2023/jasmin-wrapped/&quot;&gt;like I did last year&lt;/a&gt;, I’ll use a free-form approach this year.&lt;/p&gt;
&lt;p&gt;tl;dr: 2024 was &lt;strong&gt;fucking stressful&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;things-i-wanted-to-do-but-haven-t-done&quot;&gt;Things I wanted to do, but haven’t done&lt;/h2&gt;
&lt;p&gt;I wanted to exercise more. Maybe it was my fault in the first hand that I haven’t set clear goals
for that. Anyway, it didn’t work out as expected. I should try to do it more. It’s just… hard.
Moving to a new city (more on that later) surely haven’t made it easier.&lt;/p&gt;
&lt;p&gt;Next year it’s gonna work out, I’m sure. 🫣&lt;/p&gt;
&lt;h2 id=&quot;things-that-happened&quot;&gt;Things that happened…&lt;/h2&gt;
&lt;h3 id=&quot;at-work&quot;&gt;…at work&lt;/h3&gt;
&lt;p&gt;Originally hired as a software engineer, I’m now a “cloud platform engineer” and therefore working
together with the team on all things Kubernetes, Terraform and so on. The whole cloud business
bingo. And although we had some &lt;strong&gt;pretty rough times&lt;/strong&gt; in the past couple of months
(&lt;a href=&quot;https://anexia.com/en/company/careers&quot;&gt;wanna join? we’re hiring!&lt;/a&gt;), I’m very grateful to have such
a wonderful team. It’s unfortunate that some of them had to leave for personal reasons. In case
you’re reading this, Z. and M.: I really enjoyed the time with you. You both taught me sooo much and
I can only hope that you’re doing well right now.&lt;/p&gt;
&lt;p&gt;There are ideas in the room that could make the work even more exciting. Hopefully I’ll be able to
mention them in the recap for next year. 👀&lt;/p&gt;
&lt;p&gt;Anyway, Kubernetes is actually an interesting piece of software and I do understand it’s appealings
to the industry now. And yet, I hope that something else might step into the middle ground between
“doing stuff by hand” and “running a Kubernetes cluster”, especially for a diverse set of hosts. (I
have some ideas on that, might write a blog post about them one day.)&lt;/p&gt;
&lt;h3 id=&quot;mentally&quot;&gt;…mentally&lt;/h3&gt;
&lt;p&gt;Another thing I realised more often this year is the stability of my mental health. I’m still
stunned how much more enjoyable the life can be when you’re not constantly depressed. (say whaaaat?)
Therapy did a lot and I’m unbelievably grateful that I had such a good therapist.&lt;/p&gt;
&lt;p&gt;Nonetheless, I almost burned out. I can only hope that it won’t repeat next year.&lt;/p&gt;
&lt;p&gt;This year I chose to use more time for myself and only for myself. It was and is one of the better
decisions I made this year.&lt;/p&gt;
&lt;h3 id=&quot;in-my-friends-circle&quot;&gt;…in my friends circle&lt;/h3&gt;
&lt;p&gt;It just expanded. I made new friends in 2024. And I &lt;em&gt;think&lt;/em&gt; that I &lt;em&gt;might&lt;/em&gt; have resolved the open
conflicts with my ex. Not sure about that yet, but that’s a thing I’m definitely going to clear out
this year, sooo: 7 days to go, I’d say. 🫢&lt;/p&gt;
&lt;h3 id=&quot;on-my-projects&quot;&gt;…on my projects&lt;/h3&gt;
&lt;p&gt;The fediverse instance got a new administrator and moderator. But I also had to take a break from
the fediverse, because the moderation work wasn’t good for my wellbeing. I’m still trying to
pinpoint the exact cause, but so far it’s been likely that it’s a combination of the constant load
and, more importantly, the cultural “need” to respond to anything within a timely manner. Especially
the latter got incredibly hard, because most of the times things are seen as black or white with no
nuance in between. And yea, this also applies to some actions I did in the past.&lt;/p&gt;
&lt;p&gt;I also loosened the ties with my hackerspace this year. Due to the move into a new city, it wouldn’t
have been responsible to continue the work in the awareness team there. Less responsibilities, more
me time. It was a good decision. Yet, I miss a lot of the entities there. 🥺&lt;/p&gt;
&lt;p&gt;All other projects of 2024 we’re unfortunately postponed for… idk for how long. 😕&lt;/p&gt;
&lt;h2 id=&quot;plans-for-2025&quot;&gt;Plans for 2025&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Hopefully(!) launch a new product at work.&lt;/li&gt;
&lt;li&gt;Get a new bicycle.&lt;/li&gt;
&lt;li&gt;Tighten the existing friendships.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s it for this year, fellas. See you next year 👋&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Just migrated the queer.group S3 to Hetzner Object Storage</title>
    <link href="https://jasminchen.dev/notes/2024/migration-to-hetzner-s3/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>25f2c49faa48b27c1fb55c2978a01e5ccf58367f</id>
    <content type="html">&lt;p&gt;I planned a lot of things for today. But as usual, if you have plans, your head just decides to say:
&lt;em&gt;nope, not today.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A little benefit of this is the fact that: Because I am tired, I also had no spoons for overthinking
technical problems. As a result, I thought that it is a pretty good idea today to migrate the S3
storage for &lt;a href=&quot;https://queer.group&quot;&gt;the Mastodon instance I’m administering&lt;/a&gt; to a different provider,
in this case: &lt;strong&gt;Hetzner Object Storage&lt;/strong&gt;, which is just a fancy name for S3 on Ceph.&lt;/p&gt;
&lt;h2 id=&quot;why-the-switch&quot;&gt;Why the switch?&lt;/h2&gt;
&lt;p&gt;The current provider, &lt;a href=&quot;https://backblaze.com&quot;&gt;Backblaze&lt;/a&gt; had several problems. First and foremost,
it’s an US-based company. While I usually don’t have problems with this, and also leveraged their
hosting location in the EU, it always feels a bit better to use a provider in the EU.&lt;/p&gt;
&lt;p&gt;I am aware of &lt;a href=&quot;https://www.scaleway.com/&quot;&gt;Scaleway&lt;/a&gt; (French-based) and their offerings, but to be
honest: &lt;strong&gt;Their user interface is kinda overwhelming.&lt;/strong&gt; Yes, they offer a lot of products and
features, but to me, they offer &lt;em&gt;too&lt;/em&gt; much. I’m sure that they’re great and all, but one big benefit
Hetzner always provided to me is: &lt;em&gt;simplicity&lt;/em&gt;. A carefully designed user interface, just enough
options to be capable of doing everything I need and transparent billing.&lt;/p&gt;
&lt;p&gt;Plus, one additional benefit that definitely shouldn’t be underestimated: Since it is fully
integrated into the Hetzner billing and authentication infrastructure, I have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one account less to worry about,&lt;/li&gt;
&lt;li&gt;one UI less to understand (even if everything is managed via OpenTofu),&lt;/li&gt;
&lt;li&gt;and one thing less to document.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since Hetzner already allows you to invite others to your projects, my fellow co-admins, &lt;em&gt;gim&lt;/em&gt; and
&lt;em&gt;Nina&lt;/em&gt; now see the S3 buckets as well without any action needed from my side.&lt;/p&gt;
&lt;h3 id=&quot;aren-t-you-risking-vendor-lock-in-there&quot;&gt;Aren’t you risking vendor lock-in there?&lt;/h3&gt;
&lt;p&gt;Yes, actually, I do. The whole instance including the media files is now hosted on Hetzner
infrastructure. The backups are excluded, but I might transfer them as well. This increases the risk
for shutdowns a lot,
&lt;a href=&quot;https://jasminchen.dev/articles/2024/pruederie-hetzner/index.md&quot;&gt;if someone reports my nipples or something&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I trust Hetzner in so far that they provide me some time to migrate off their infrastructure, in
case that happens.&lt;/p&gt;
&lt;p&gt;At this point it actually is pure irony that I work for an Austrian cloud provider and don’t use any
of their products, because the UI sucks. But hey, not my fault. &#39;^^&lt;/p&gt;
&lt;h2 id=&quot;the-migration-process&quot;&gt;The migration process&lt;/h2&gt;
&lt;p&gt;In order to provide a smooth transition from one provider to another without too much impact, I
decided to do the migration in the following order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change the proxy to ask the new S3 backend first, fallback to the current&lt;/li&gt;
&lt;li&gt;Adjust Mastodon configuration to point to the new bucket&lt;/li&gt;
&lt;li&gt;Copy all files from the current to the new bucket&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;adjust-nginx-to-use-two-s3-backends-for-proxying&quot;&gt;Adjust nginx to use two S3 backends for proxying&lt;/h3&gt;
&lt;p&gt;Since the media files are
&lt;a href=&quot;https://docs.joinmastodon.org/admin/optional/object-storage-proxy/&quot;&gt;already proxied behind nginx&lt;/a&gt;,
it was pretty easy to add the Hetzner S3 as one additional &lt;em&gt;named location&lt;/em&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-nginx&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;location&lt;/span&gt; @s3hetzner&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;limit_except&lt;/span&gt; GET&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;deny&lt;/span&gt; all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;resolver&lt;/span&gt; 8.8.8.8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; Host BUCKET_NAME.fsn1.your-objectstorage.com&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; Connection &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; Authorization &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; Set-Cookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Access-Control-Allow-Origin&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Access-Control-Allow-Methods&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Access-Control-Allow-Headers&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; x-amz-id-2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; x-amz-request-id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; x-amz-meta-server-side-encryption&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; x-amz-server-side-encryption&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; x-amz-bucket-region&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; x-amzn-requestid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  // &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Note:&lt;/span&gt; The following two headers might not longer be needed after the beta phase.
  proxy_hide_header x-debug-bucket&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_hide_header&lt;/span&gt; x-debug-backend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_ignore_headers&lt;/span&gt; Set-Cookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_pass&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$s3_backend&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$uri&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; // &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Note&lt;/span&gt; for the reader: Set this to your own value.
  proxy_intercept_errors &lt;span class=&quot;token boolean&quot;&gt;on&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  // &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Note:&lt;/span&gt; change this to &lt;span class=&quot;token boolean&quot;&gt;off&lt;/span&gt; as soon as the migration is done.

  // Enable proxying via TLS
  proxy_ssl_protocols TLSv1.3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_ssl_name&lt;/span&gt; BUCKET_NAME.fsn1.your-objectstorage.com&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_ssl_server_name&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;on&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_cache&lt;/span&gt; CACHE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_cache_valid&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;48h&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_cache_use_stale&lt;/span&gt; error timeout updating http_500 http_502 http_503 http_504&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_cache_lock&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;on&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;expires&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1y&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;add_header&lt;/span&gt; Cache-Control public&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;add_header&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Access-Control-Allow-Origin&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;*&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;add_header&lt;/span&gt; X-Cache-Status &lt;span class=&quot;token variable&quot;&gt;$upstream_cache_status&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;add_header&lt;/span&gt; X-Content-Type-Options nosniff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;add_header&lt;/span&gt; Content-Security-Policy &lt;span class=&quot;token string&quot;&gt;&quot;default-src &#39;none&#39;; form-action &#39;none&#39;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  // Fallback to the existing backend, if there&#39;s &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;no&lt;/span&gt; match.
  error_page &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt; = @s3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;error_page&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt; = @s3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also had to change the default location at the top to the new location.&lt;/p&gt;
&lt;pre class=&quot;language-nginx&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;location&lt;/span&gt; /&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;try_files&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$uri&lt;/span&gt; @s3hetzner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;a-little-lesson-i-learned-along-the-way&quot;&gt;A little lesson I learned along the way&lt;/h4&gt;
&lt;p&gt;The following snippet did not work, since it looks like that always the last location is chosen.
That’s a problem, since new uploads are going to be made to the new bucket in the next step.&lt;/p&gt;
&lt;pre class=&quot;language-nginx&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;location&lt;/span&gt; /&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;try_files&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$uri&lt;/span&gt; @s3 @s3hetzner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;telling-mastodon-to-use-the-new-s3&quot;&gt;Telling Mastodon to use the new S3&lt;/h3&gt;
&lt;p&gt;That step is pretty simple. Adjust
&lt;a href=&quot;https://docs.joinmastodon.org/admin/optional/object-storage/&quot;&gt;the S3 settings&lt;/a&gt; inside the
&lt;code&gt;.env.production&lt;/code&gt; file to point to the new location and restart all Mastodon processes. For the
record, I use the following values:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token key attr-name&quot;&gt;S3_ENABLED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;S3_BUCKET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;BUCKET_NAME&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;S3_ENDPOINT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;https://fsn1.your-objectstorage.com&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;S3_ALIAS_HOST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;pdn.queer.group&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After I restarted Mastodon, I quickly uploaded a random image with the visibility set to private and
posted it. It worked as intended, so I went on to the next step.&lt;/p&gt;
&lt;h3 id=&quot;mirroring-the-old-bucket-to-the-new-onini&quot;&gt;Mirroring the old bucket to the new onini&lt;/h3&gt;
&lt;p&gt;At this point, the functionality of Mastodon itself was no longer impacted. Existing media files
were working and new ones could be uploaded as well. What remained was the migration of the
remaining data.&lt;/p&gt;
&lt;p&gt;For this I used the &lt;a href=&quot;https://min.io/docs/minio/linux/reference/minio-mc.html&quot;&gt;Minio client&lt;/a&gt;, known
as &lt;code&gt;mc&lt;/code&gt; on the command line. Since &lt;a href=&quot;https://lix.systems&quot;&gt;Lix&lt;/a&gt; is installed on this machine, I did it
via &lt;code&gt;nix-shell&lt;/code&gt;. This spawns a new shell with the &lt;code&gt;minio-client&lt;/code&gt; package installed in it.&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ nix-shell &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; minio-client&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, I setup two aliases and mirrored the “folders” using &lt;code&gt;mc mirror&lt;/code&gt;.&lt;/p&gt;
&lt;!-- &lt;note @header=&quot;About S3 folders&quot;&gt; --&gt;
&lt;!-- Technically, folders/directories aren&#39;t a concept in S3, yet they&#39;re usually displayed as such in user interfaces, since users are familiar with the concept. --&gt;
&lt;!-- That&#39;s why I stick to the concept in the rest of this article as well. --&gt;
&lt;!-- &lt;/note&gt; --&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Replace `old-bucket-name` and `new-bucket-name` with the actual names of your buckets.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#   -a preserves any attributes (shouldn&#39;t be needed, but better safe than sorry)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#   --retry retries the upload, in case an error occurs for whatever reason.&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;mc&lt;/span&gt; mirror &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--retry&lt;/span&gt; backblaze/old-bucket-name/accounts/ hetzner/new-bucket-name/accounts/
$ &lt;span class=&quot;token function&quot;&gt;mc&lt;/span&gt; mirror &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--retry&lt;/span&gt; backblaze/old-bucket-name/backups/ hetzner/new-bucket-name/backups/
$ &lt;span class=&quot;token function&quot;&gt;mc&lt;/span&gt; mirror &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--retry&lt;/span&gt; backblaze/old-bucket-name/custom_emojis/ hetzner/new-bucket-name/custom_emojis/
$ &lt;span class=&quot;token function&quot;&gt;mc&lt;/span&gt; mirror &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--retry&lt;/span&gt; backblaze/old-bucket-name/images/ hetzner/new-bucket-name/images/
$ &lt;span class=&quot;token function&quot;&gt;mc&lt;/span&gt; mirror &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--retry&lt;/span&gt; backblaze/old-bucket-name/media_attachments/ hetzner/new-bucket-name/media_attachments/
$ &lt;span class=&quot;token function&quot;&gt;mc&lt;/span&gt; mirror &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--retry&lt;/span&gt; backblaze/old-bucket-name/site_uploads/ hetzner/new-bucket-name/site_uploads/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those are all the smaller folders, which – depending on your instance – might only consume a
couple of gigabytes. The biggest folder, the &lt;code&gt;cache&lt;/code&gt; is likely &lt;em&gt;substantially&lt;/em&gt; larger than the rest.
In my case, it’s a whopping 250GB! Therefore I leveraged
&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd-inhibit.html&quot;&gt;&lt;code&gt;systemd-inhibit&lt;/code&gt;&lt;/a&gt; in
order to prevent my notebook from sleeping.&lt;/p&gt;
&lt;p&gt;And yes, I am using my notebook for the migration, even though it’s neither the fastest nor the most
reliable solution. But I really don’t care for this one-time job to follow “industry best
practices”, I just want the job to be done eventually one day.&lt;/p&gt;
&lt;p&gt;Now I had to wait for hours and hours, until the migration was done. Eventually I wrapped it inside
of &lt;code&gt;systemd-run&lt;/code&gt; and committed a crime with this level of nesting, but hey, it works. :p&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ systemd-run &lt;span class=&quot;token parameter variable&quot;&gt;-u&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sync-mastodon-assets&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--user&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  systemd-inhibit &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  nix-shell &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; minio-client &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--run&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;mc mirror backblaze/old-bucket-name/cache/ hetzner/new-bucket-name/cache/ --retry -a&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Eleventy is nice, actually</title>
    <link href="https://jasminchen.dev/notes/2024/eleventy-is-nice-actually/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>e5d78d2aacf816f5a859eb12ee6fe3bf8bd875da</id>
    <content type="html">&lt;p&gt;It is time for a rewrite again. Why writing blog posts when you can just refactor your homepage
again and again and again…&lt;/p&gt;
&lt;p&gt;Well, this time I chose &lt;a href=&quot;https://11ty.dev&quot;&gt;Eleventy&lt;/a&gt; (11ty) for it. The primary reason behind this
migration was: stable releases. The previous version of this homepage was using
&lt;a href=&quot;https://gohugo.io&quot;&gt;Hugo&lt;/a&gt;. And although Hugo is nice, if you’re already in the Go ecosystem, the
lack of a stable version really put me off this time. You know, sometimes I just want to do
something on my homepage. And when I can’t, because something changed again and I have to fix my
templates (again), I’ll just get distracted.&lt;/p&gt;
&lt;p&gt;And that’s why I ditched Hugo. Now, you could ask me: why not &lt;a href=&quot;https://www.getzola.org/&quot;&gt;Zola&lt;/a&gt; or
some other static site generator that’s been statically compiled? Why 11ty, which itself is written
in JavaScript?&lt;/p&gt;
&lt;p&gt;To be honest: because it &lt;em&gt;feels&lt;/em&gt; correct. It is the language for the web. And it’s not even
fundamentally slower than Hugo. Hugo took like 1 second, 11ty takes 1.1 or so. Never measured it,
because: it doesn’t matter for me.&lt;/p&gt;
&lt;h2 id=&quot;benefits-of-the-migration&quot;&gt;Benefits of the migration&lt;/h2&gt;
&lt;p&gt;With the rewrite, I also took the opportunity and ditched Tailwind CSS. I wanted to refresh my CSS
skills, since a lot of good stuff was added to it. I’m a big fan of the &lt;code&gt;:is()&lt;/code&gt; selector!&lt;/p&gt;
&lt;p&gt;Eleventy also has the ability to compute &lt;em&gt;bundles&lt;/em&gt; of CSS/JavaScript per page. It’s plain, but it
works pretty good. You can see it on the individual blog post pages, which get progressively
enhanced with anchor links if JS is available.&lt;/p&gt;
&lt;p&gt;Oh, and another big improvement: I no longer minify the outputs. I think that in the time of HTTP/2
and compression, it doesn’t add that much of a benefit. The CSS is just 2KB big, it’s not even
shipped with one of those “reset stylesheets”. Go ahead,
&lt;a href=&quot;https://jasminchen.dev/css/index.css&quot;&gt;look at this smol stylesheet&lt;/a&gt;! It is so frickin’ simple, I love it.&lt;/p&gt;
&lt;p&gt;With the removal of the minification, you can just open the source code of this page and learn from
it! For me, that’s a much better feature than, idk, 10 milliseconds of reduced transfer time.&lt;/p&gt;
&lt;h2 id=&quot;still-missing&quot;&gt;Still missing&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;The dark theme. It’s on my todo list.&lt;/li&gt;
&lt;li&gt;The project list page: I’ve ditched it completely for now, gonna add it back later on.&lt;/li&gt;
&lt;li&gt;Proper &lt;code&gt;lang&lt;/code&gt; tags for German content.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;plans&quot;&gt;Plans&lt;/h2&gt;
&lt;p&gt;One thing I really want to add to this page is some touch of personality. Right now, it feels pretty
bare. I already have ideas for some eastereggs, let’s see when I actually implement them
🙈.&lt;/p&gt;
&lt;p&gt;Anyway, that’s it for now. If you have feedback of any kind, just mention me on the fediverse or via
mail.&lt;/p&gt;
&lt;p&gt;Over and out&lt;br&gt; Jasmin&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>GPN22: Lektionen aus 1,5 Jahren Fediverse-Moderation und -Administration</title>
    <link href="https://jasminchen.dev/notes/2024/gpn22-talk-fediverse-moderation/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>eb305a9d8cc869f3754705e4057a39f84977c37a</id>
    <content type="html">&lt;p&gt;Ehrlich gesagt, kann ich immer noch nicht wirklich glauben, dass ich dieses Jahr auf der GPN einen
Talk gehalten hab. Aber nun, äh, hab ich.&lt;/p&gt;
&lt;p&gt;Jetzt steht meine Meinung im Internet. Bissel gruselig ist das ja schon. Aber nun. ^^&lt;/p&gt;
&lt;p&gt;Der
&lt;a href=&quot;https://media.ccc.de/v/gpn22-318-lektionen-aus-1-5-jahren-fediverse-moderation-und-administration&quot;&gt;Talk ist auf media.ccc.de&lt;/a&gt;
abrufbar. Und die Folien hab ich separat für diesen Post nochmal hochgeladen. Also,
&lt;a href=&quot;https://jasminchen.dev/notes/2024/gpn22-talk-fediverse-moderation/Folien-GPN22-nachtjasmin-Lektionen-Fediverse-Moderation.pdf&quot;&gt;einmal die Folien als PDF&lt;/a&gt;,
ebenfalls unter CC-BY 4.0 lizensiert.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Analysing the source code of Truth Social</title>
    <link href="https://jasminchen.dev/notes/2024/analysing_the_truthsocial_source_code/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>c358e368ced8f5747a8ee707f19fb4a7cfe54530</id>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;Truth Social is America’s “Big-Tent” social media platform that encourages an open, free, and
honest global conversation without discriminating against political ideology.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After &lt;a href=&quot;https://boehs.org/node/truth-social&quot;&gt;Evan Boehs&lt;/a&gt; got the freeze peach connosieurs of Truth
Social to release their source code again, I was immediately hooked. Not because this sparks joy,
but because I’m a silly curious pup. 🐶&lt;/p&gt;
&lt;p&gt;This is more or less just a stream of thoughts, probably a bit more structured than by thoughts.&lt;/p&gt;
&lt;h2 id=&quot;which-version-are-we-running-on&quot;&gt;Which version are we running on?&lt;/h2&gt;
&lt;p&gt;Since I’m involved with the codebase of Mastodon from time to time (&lt;em&gt;cough&lt;/em&gt;), I knew immediately
where to look. &lt;code&gt;lib/mastodon/version.rb&lt;/code&gt;. According to this file, truth.social is still based upon
Mastodon 3.4.1. This isn’t new, but still, I’m fascinated that they haven’t incorporated at least
the other changes from the &lt;code&gt;stable/version-3.4&lt;/code&gt; or even the &lt;code&gt;stable/version-3.5&lt;/code&gt; branches.&lt;/p&gt;
&lt;h2 id=&quot;did-they-adapt-the-source-code-and-yes-to-what-extent&quot;&gt;Did they adapt the source code and yes, to what extent?&lt;/h2&gt;
&lt;p&gt;I’ve continued by cloning the Mastodon source code with &lt;code&gt;3.4.1&lt;/code&gt; as the tag. From there, I started to
compare the Truth Social source code against the Mastodon one using my beloved
&lt;a href=&quot;https://github.com/dandavison/delta&quot;&gt;delta&lt;/a&gt;. A simple&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delta ./truthsocial/source ./mastodon-3.4.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is doing the trick for the following report.&lt;/p&gt;
&lt;h2 id=&quot;integration-of-the-prometheus-exporter&quot;&gt;Integration of the Prometheus exporter&lt;/h2&gt;
&lt;p&gt;Apparently they have added the &lt;code&gt;prometheus_exporter&lt;/code&gt; library to the dependencies. Unfortunately, it
doesn’t seem to be available under &lt;code&gt;https://truthsocial.com/metrics&lt;/code&gt;, that would’ve been too easy.
According to the &lt;code&gt;./app/lib/prometheus/application_exporter.rb&lt;/code&gt;, they’re using it for the following
metrics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;number of created statuses&lt;/li&gt;
&lt;li&gt;number of retruths&lt;/li&gt;
&lt;li&gt;number of replies&lt;/li&gt;
&lt;li&gt;number of favourites&lt;/li&gt;
&lt;li&gt;number of reports&lt;/li&gt;
&lt;li&gt;number of blocks&lt;/li&gt;
&lt;li&gt;number of login attempts&lt;/li&gt;
&lt;li&gt;number of registrations&lt;/li&gt;
&lt;li&gt;number of uploaded media files&lt;/li&gt;
&lt;li&gt;number of accounts following account&lt;/li&gt;
&lt;li&gt;number of accounts unfollowing accounts&lt;/li&gt;
&lt;li&gt;number of posted links&lt;/li&gt;
&lt;li&gt;number of approved users&lt;/li&gt;
&lt;li&gt;number of ad impressions&lt;/li&gt;
&lt;li&gt;number of chats&lt;/li&gt;
&lt;li&gt;number of chat messages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Besides the custom metrics mentioned above, it is also integrated into Sidekiq, as a middleware for
each request and probably some other places. So, they’re using Prometheus metrics for observability.
Since &lt;a href=&quot;https://newrelic.com/&quot;&gt;New Relic&lt;/a&gt; is also mentioned in the code, they’re using their services
for monitoring and stuff.&lt;/p&gt;
&lt;h2 id=&quot;rss-feed-mocking&quot;&gt;RSS feed mocking&lt;/h2&gt;
&lt;p&gt;Mastodon has this neat feature, where you can follow the &lt;em&gt;public&lt;/em&gt; posts of any profile using a RSS
reader. Apparently truth.social does not like this, at least they’ve added the
&lt;code&gt;./app/controllers/api/mock/feeds_controller.rb&lt;/code&gt; whose sole work is to create empty RSS feeds.&lt;/p&gt;
&lt;h2 id=&quot;feeds&quot;&gt;Feeds&lt;/h2&gt;
&lt;p&gt;But they also have a &lt;code&gt;./app/controllers/api/v1/feeds_controller.rb&lt;/code&gt; which seems to be a different
feature. And &lt;code&gt;DEFAULT_FEEDS_SIZE = 18&lt;/code&gt; with a value of 18 is not a dogwhistle. no no. it’s all fine.
/sarcasm&lt;/p&gt;
&lt;p&gt;Following &lt;code&gt;./db/seeds/feeds/feeds.sql&lt;/code&gt;, this feature is similar to the timelines of Mastodon. We
have the following feeds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Following: A chronological timeline of Truths from accounts you follow.&lt;/li&gt;
&lt;li&gt;For You: Truths we think you’ll be interested in&lt;/li&gt;
&lt;li&gt;Groups: A chronological timeline of Truths from the Groups you’ve joined.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ios-android-specific-handling&quot;&gt;iOS-/Android-specific handling&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;app/controllers/api/base_controller.rb&lt;/code&gt; got several notable changes. Since I don’t know whether
some of them we’re cherry-picked commits (and I’m too lazy to find out right now), I’ll focus on the
stuff that’s definitely a change of the source code by them.&lt;/p&gt;
&lt;p&gt;Apparently, Truth Social contains code for SMS verification of users. And some iOS/Android specific
stuff for device registration.&lt;/p&gt;
&lt;h2 id=&quot;pleroma-inside-my-mastodon-codebase&quot;&gt;Pleroma? Inside my Mastodon codebase?&lt;/h2&gt;
&lt;p&gt;They added code to handle Pleroma-specific API requests.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;app/controllers/api/pleroma/
&lt;ul&gt;
&lt;li&gt;accounts_controller.rb&lt;/li&gt;
&lt;li&gt;user_settings_controller.rb&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;app/controllers/api/v1/pleroma/
&lt;ul&gt;
&lt;li&gt;chats
&lt;ul&gt;
&lt;li&gt;events_controller.rb&lt;/li&gt;
&lt;li&gt;messages_controller.rb&lt;/li&gt;
&lt;li&gt;reactions_controller.rb&lt;/li&gt;
&lt;li&gt;search_controller.rb&lt;/li&gt;
&lt;li&gt;silences_controller.rb&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;chats_controller.rb&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;app/controllers/api/v2/pleroma/
&lt;ul&gt;
&lt;li&gt;chats
&lt;ul&gt;
&lt;li&gt;events_controller.rb&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(surprise surprise, the nazis are supporting each other. If I should name one software that yells
“hello, please block me” as a Fediverse moderator, it’d be Pleroma.)&lt;/p&gt;
&lt;p&gt;The compatibility with Pleroma is also mentioned in the
&lt;code&gt;app/controllers/api/v1/accounts/credentials_controller.rb&lt;/code&gt;. This is likely for the support of
Soapbox as a frontend, also mentioned with the OAuth secret inside &lt;code&gt;db/seeds.rb&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;advertisements&quot;&gt;Advertisements&lt;/h2&gt;
&lt;p&gt;Yes, they also added advertisements, including their
&lt;code&gt;app/controllers/api/v4/truth/ads_controller.rb&lt;/code&gt;. In order to built on top of this feature, it’s
also mentioned across several places within the codebase, e.g. in the
&lt;code&gt;app/controllers/api/v1/accounts/credentials_controller.rb&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can also report an ad if you don’t like it. They are using the following advertisement networks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On iOS they’re using the
&lt;a href=&quot;https://developer.apple.com/documentation/storekit/skadnetwork&quot;&gt;SkAdNetwork API from Apple&lt;/a&gt;,
which is accessing a new endpoint in &lt;code&gt;.well_known&lt;/code&gt;
(&lt;code&gt;./app/controllers/well_known/skadnetwork_controller.rb&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://revcontent.com&quot;&gt;revcontent&lt;/a&gt;, referenced in
&lt;code&gt;./app/workers/track_revcontent_ad_impressions_worker.rb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ads.rumble.com/&quot;&gt;Rumble&lt;/a&gt;, which is referenced in
&lt;code&gt;./app/workers/track_rumble_ad_impressions_worker.rb&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s also an API key(?) for one of those in &lt;code&gt;./app/views/layouts/application.html.haml&lt;/code&gt; in
line 46.&lt;/p&gt;
&lt;h2 id=&quot;social-networks-now-on-the-tv&quot;&gt;Social networks, now on the TV!&lt;/h2&gt;
&lt;p&gt;Truth Social has a lot of TV integrations. Yes, including an
&lt;a href=&quot;https://en.wikipedia.org/wiki/Electronic_program_guide&quot;&gt;EPG&lt;/a&gt;. They learned from the
&lt;a href=&quot;https://en.wikipedia.org/wiki/Nazi_Party&quot;&gt;Nazi Party&lt;/a&gt; and their propaganda networks, that’s for
sure.&lt;/p&gt;
&lt;p&gt;Affected files/folders:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;./app/controllers/api/v1/admin/tv/&lt;/li&gt;
&lt;li&gt;./app/controllers/api/v1/tv/&lt;/li&gt;
&lt;li&gt;./app/models/tv_account.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_carousel.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_channel.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_channel_account.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_device_session.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_program.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_program_status.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_program_temporary.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_reminder.rb&lt;/li&gt;
&lt;li&gt;./app/models/tv_status.rb&lt;/li&gt;
&lt;li&gt;./app/serializers/rest/tv_program_serializer.rb&lt;/li&gt;
&lt;li&gt;./app/serializers/rest/v2/tv_carousel_serializer.rb&lt;/li&gt;
&lt;li&gt;./app/serializers/rest/v2/tv_channel_guide_serializer.rb&lt;/li&gt;
&lt;li&gt;./app/serializers/rest/v2/tv_program_serializer.rb&lt;/li&gt;
&lt;li&gt;./app/services/p_tv/&lt;/li&gt;
&lt;li&gt;./app/workers/scheduler/tv_create_program_records_scheduler.rb&lt;/li&gt;
&lt;li&gt;./app/workers/scheduler/tv_refetch_channels_list_scheduler.rb&lt;/li&gt;
&lt;li&gt;./app/workers/tv_accounts_create_worker.rb&lt;/li&gt;
&lt;li&gt;./app/workers/tv_accounts_login_worker.rb&lt;/li&gt;
&lt;li&gt;./app/workers/tv_create_tv_program_status_worker.rb&lt;/li&gt;
&lt;li&gt;./app/workers/tv_program_reminder_notification_worker.rb&lt;/li&gt;
&lt;li&gt;./public/tv/&lt;/li&gt;
&lt;li&gt;./spec/controllers/api/v1/admin/tv/&lt;/li&gt;
&lt;li&gt;./spec/controllers/api/v1/tv/&lt;/li&gt;
&lt;li&gt;./spec/fabricators/tv_account_fabricator.rb&lt;/li&gt;
&lt;li&gt;./spec/fabricators/tv_channel_account_fabricator.rb&lt;/li&gt;
&lt;li&gt;./spec/fabricators/tv_channel_fabricator.rb&lt;/li&gt;
&lt;li&gt;./spec/fabricators/tv_device_session_fabricator.rb&lt;/li&gt;
&lt;li&gt;./spec/workers/tv_accounts_create_worker_spec.rb&lt;/li&gt;
&lt;li&gt;./spec/workers/tv_accounts_login_worker_spec.rb&lt;/li&gt;
&lt;li&gt;./spec/workers/tv_create_tv_program_status_worker_spec.rb&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;According to the &lt;code&gt;./app/serializers/rest/tv_program_serializer.rb&lt;/code&gt;, some of the content is hosted on
https://vstream.truthsocial.com.&lt;/p&gt;
&lt;h3 id=&quot;they-re-really-focusing-on-videos&quot;&gt;They’re really focusing on videos.&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;./app/models/media_attachment.rb&lt;/code&gt;, they increased the maximum size for videos from 40 to
450(!) MB. Furthermore, they’re also allowing &lt;code&gt;application/octet-stream&lt;/code&gt; as a content type for
videos. This might end up in a vulnerability, but I think they’re preventing this by performing a
&lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_file_signatures&quot;&gt;magic byte detection&lt;/a&gt; in
&lt;code&gt;./lib/paperclip/media_type_spoof_detector_extensions.rb&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;rumble-com&quot;&gt;rumble.com&lt;/h3&gt;
&lt;p&gt;Speaking of videos, apparently you can also upload directly to rumble.com (is this just another
YouTube clone?). The logic for that is in: &lt;code&gt;./app/services/concerns/upload_video_concern.rb&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;newsmax&quot;&gt;Newsmax&lt;/h3&gt;
&lt;p&gt;Newsmax, a TV broadcasting company that describes itself as “real news for real people” (wtf) is
also tightly integrated into Truth Social. You can fetch the &lt;em&gt;electronic program guide&lt;/em&gt; (EPG)
directly from Truth Social, according to the
&lt;code&gt;./app/services/p_tv/programme_guides/newsmax_service.rb&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;one-america-news-network-oann&quot;&gt;One America News Network (OANN)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;One America News Network, also known as One America News, is a far-right, pro-Trump cable news and
political opinion commentary channel founded by Robert Herring Sr. and owned by Herring Networks,
Inc., that launched on July 4, 2013. The network is headquartered in San Diego, California, and
operates news bureaus in Washington, D.C., and New York City. –
&lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=One_America_News_Network&amp;amp;oldid=1218170568&quot;&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s also integrated into the EPG, see &lt;code&gt;./app/services/p_tv/programme_guides/oan_service.rb&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;real-america-s-voice-rav&quot;&gt;Real America’s Voice (RAV)&lt;/h3&gt;
&lt;p&gt;EPG also integrated into Truth Social, defined in
&lt;code&gt;./app/services/p_tv/programme_guides/rav_service.rb&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Real America’s Voice is a right-wing to far-right streaming, cable and satellite television
channel founded in 2020 and owned by Robert J. Sigg. The network and online presences have
promoted right-wing and far-right conspiracy theories, including COVID-19 misinformation, 2020
election conspiracies, and QAnon. –
&lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Real_America%27s_Voice&amp;amp;oldid=1216014110&quot;&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;weather-nation-wn&quot;&gt;Weather Nation (WN)&lt;/h3&gt;
&lt;p&gt;Weather Nation, whose operator is also responsible for RAV, is also integrated in
&lt;code&gt;./app/services/p_tv/programme_guides/wn_service.rb&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;api-endpoints-for-follower-information&quot;&gt;API endpoints for follower information?&lt;/h2&gt;
&lt;p&gt;They also added controllers to return the follow-relationships or account information as JSON.
Sadly, for us antifascists, it’s locked down to administrators with write(?) permissions. The
controllers can be found in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;./app/controllers/api/v1/admin/accounts/follows_controller.rb&lt;/li&gt;
&lt;li&gt;./app/controllers/api/v1/admin/accounts/statuses_controller.rb&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;mass-import-of-accounts&quot;&gt;Mass-import of accounts&lt;/h2&gt;
&lt;p&gt;Did they actually add CSV imports to import users? Apparently so. I can only assume about the
reasoning behind it, but yeah, line 71 of the
&lt;code&gt;./app/controllers/api/v1/admin/accounts_controller.rb&lt;/code&gt; actually suggests that:&lt;/p&gt;
&lt;pre class=&quot;language-ruby&quot;&gt;&lt;code class=&quot;language-ruby&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;not_ready_for_approval&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ready_by_csv_import&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when you continue, you actually realise that they built their mass-import tool to create new
users. And also, to help the staff, they also added the possibility to process multiple accounts in
advance (&lt;code&gt;./app/controllers/api/v1/admin/bulk_account_actions_controller.rb&lt;/code&gt;), which only seems to
be used to trigger the account validation using SMS again.&lt;/p&gt;
&lt;h2 id=&quot;chats&quot;&gt;Chats&lt;/h2&gt;
&lt;p&gt;“Plan the next attack on the White House, exclusively on Truth Social!” could be a potential
advertisement for this feature. Added in
&lt;code&gt;./app/controllers/api/v1/admin/chat_messages_controller.rb&lt;/code&gt;, they’ve build a chat feature into
their social network.&lt;/p&gt;
&lt;p&gt;Their chat feature has everything a chat needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reactions&lt;/li&gt;
&lt;li&gt;a functioning search&lt;/li&gt;
&lt;li&gt;read receipts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the background they’re using a RabbitMQ queue system (&lt;code&gt;./app/lib/events/event_bus.rb&lt;/code&gt;) to support
that, the chats itself are stored in the database. Chat messages are limited to 500 characters.&lt;/p&gt;
&lt;h2 id=&quot;groups&quot;&gt;Groups&lt;/h2&gt;
&lt;p&gt;They have groups, just like Facebook. It actually seems to support a lot of stuff, including but not
limited to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;invites&lt;/li&gt;
&lt;li&gt;promoting users&lt;/li&gt;
&lt;li&gt;kicking out users&lt;/li&gt;
&lt;li&gt;group-specific timelines&lt;/li&gt;
&lt;li&gt;group-only posts&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;moderation&quot;&gt;Moderation&lt;/h2&gt;
&lt;h3 id=&quot;blocked-links&quot;&gt;Blocked links&lt;/h3&gt;
&lt;p&gt;Truth Social has a pattern-matching based ability to block links in
&lt;code&gt;./app/controllers/api/v1/admin/links_controller.rb&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;moderation-actions&quot;&gt;Moderation actions&lt;/h3&gt;
&lt;p&gt;Besides the already existing moderation tools of Mastodon, Truth Social has the following moderation
actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add/remove the “sensitivity” of a post (probably the same as the media sensitivity in Mastodon?)&lt;/li&gt;
&lt;li&gt;The ability to change the privacy level of a post from public to private and vice-versa.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those got added in &lt;code&gt;./app/controllers/api/v1/admin/statuses_controller.rb&lt;/code&gt;. I mean, it’s not
federating and such, still: fascinating that they added moderation features that are (partially) not
even in Mastodon itself.&lt;/p&gt;
&lt;h3 id=&quot;rules&quot;&gt;Rules&lt;/h3&gt;
&lt;p&gt;According to &lt;code&gt;./config/locales/en.yml&lt;/code&gt;, Truth Social actually has rules. Content or accounts that
does not comply with with the following rules, seems to get deleted:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Illegal activity and behavior: Content that depicts illegal or criminal acts, threats of
violence.&lt;/li&gt;
&lt;li&gt;Intellectual property infringement: Impersonating another account or business, infringing on
intellectual property rights.&lt;/li&gt;
&lt;li&gt;Sensitive content: Depictions of violence, gore, nudity.&lt;/li&gt;
&lt;li&gt;Underage content: Sexually explicit content involving underage children.&lt;/li&gt;
&lt;li&gt;Prostitution: Solicitation or advertising for illegal sexual activity or sex for hire.&lt;/li&gt;
&lt;li&gt;Privacy violations: Violate or post content that violates a person’s privacy rights.&lt;/li&gt;
&lt;li&gt;Illegal sales: Sale of or promotion of illegal drugs, counterfeit services and goods, or illegal
products and services.&lt;/li&gt;
&lt;li&gt;Doxxing: Sharing or threatening to share the private information of an individual without their
consent or breach of privacy rights of others.&lt;/li&gt;
&lt;li&gt;Spam: Fraudulent or malicious content or links, inauthentic engagement, repetitive replies,
ReTruths, or direct messages.&lt;/li&gt;
&lt;li&gt;Troll: This user is a troll and/or I dislike their content.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;moderationai&quot;&gt;“ModerationAI”&lt;/h3&gt;
&lt;p&gt;Apparently, they’re also using so-called “AI” named “ModerationAI” for moderation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We use artificial intelligence (AI) to assist our hardworking moderators, and some Truths are
flagged for deletion or marked “sensitive” by AI. While the AI we use is very good, it is not
error-proof. Assisted by technology, our moderators use their best judgment to ensure compliance
with our Terms of Service. Please give our team time to review your Truth to determine whether it
violates our Terms of Service. After a thorough review, we will reinstate the Truth or uphold its
removal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;See also: &lt;code&gt;./lib/tasks/generate_moderator_ai_admin.rake&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;federation&quot;&gt;Federation&lt;/h2&gt;
&lt;p&gt;Truth Social has removed most of the ActivityPub (AP)-related code. The inbox/outbox controllers got
removed, actions are passed the &lt;code&gt;skip_activitypub: true&lt;/code&gt; parameter if possible, et cetera. All
references to Mastodon are either replaced with “Truth Social” or just “Truth” or just removed. This
includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;links to the documentation&lt;/li&gt;
&lt;li&gt;the instance behind a profile (so &lt;code&gt;donald@truthsocial.com&lt;/code&gt; just becomes &lt;code&gt;donald&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;renaming the “Toot” button to “Truth” (lol, lmao even)&lt;/li&gt;
&lt;li&gt;removing features like:
&lt;ul&gt;
&lt;li&gt;polls,&lt;/li&gt;
&lt;li&gt;content warnings,&lt;/li&gt;
&lt;li&gt;post privacy buttons,&lt;/li&gt;
&lt;li&gt;bookmarks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The whole &lt;code&gt;app/services/activitypub&lt;/code&gt; just got cleaned of all the federation features, it’s only
used for fetching remote content now.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;please-don-t-block-us-from-scraping-your-content&quot;&gt;please don’t block us from scraping your content 🥺&lt;/h2&gt;
&lt;p&gt;For outgoing requests, they’re using a proxy by default, as seen in &lt;code&gt;./app/lib/request.rb&lt;/code&gt;. Also,
they are hiding that they’re Truth Social by omitting the instance name from the default user agent.
Usually, Mastodon requests content with a user agent that contains the following substring
&lt;code&gt;(Mastodon/1.2.3, https://example.com)&lt;/code&gt; with &lt;code&gt;1.2.3&lt;/code&gt; being the version and &lt;code&gt;https://example.com&lt;/code&gt; the
public URL of the instance.&lt;/p&gt;
&lt;p&gt;They removed that in &lt;code&gt;./lib/mastodon/version.rb&lt;/code&gt;, which also means that website administrators who
want to block Truth Social cannot do that with just the user agent.&lt;/p&gt;
&lt;h2 id=&quot;other-stuff&quot;&gt;Other stuff&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;They renamed the “toot” button to “truth”.&lt;/li&gt;
&lt;li&gt;They renamed the “reblog” button to “retruth”.&lt;/li&gt;
&lt;li&gt;Instead of stars there’s a fire emoji for likes/stars.&lt;/li&gt;
&lt;li&gt;The character limit is 1000 characters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;openapi-documentation-for-authenticated-users&quot;&gt;OpenAPI documentation (for authenticated users)&lt;/h3&gt;
&lt;p&gt;They integrated Swagger/OpenAPI into their application. If you have an account on Truth Social, you
can open the &lt;a href=&quot;https://truthsocial.com/api/docs&quot;&gt;API documentation of Truth Social&lt;/a&gt;, this got added
via the &lt;code&gt;./app/controllers/apidocs_controller.rb&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;deployment-via-gitlab-and-dokku&quot;&gt;Deployment via GitLab and Dokku&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;./app.json&lt;/code&gt;, &lt;a href=&quot;https://dokku.com/&quot;&gt;Dokku&lt;/a&gt; is mentioned. Looking further, they’re also using
the GitLab CI and a Dokku instance hosted at dokku.tmediatech.io, which is mentioned in the
&lt;code&gt;./bin/ci-pre-deploy&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;potential-sql-vulnerabilities&quot;&gt;Potential SQL vulnerabilities&lt;/h3&gt;
&lt;p&gt;They also commited a (potentially outdated) brakeman report under &lt;code&gt;./brakeman-output.json&lt;/code&gt;, which
was last executed on July 17th 2023 in &lt;code&gt;/Users/markmorales/Code/social-v1_groups&lt;/code&gt;. Said scan
mentions a lot of potential SQL injections.&lt;/p&gt;
&lt;h3 id=&quot;removal-of-the-code-of-conduct&quot;&gt;Removal of the code of conduct&lt;/h3&gt;
&lt;p&gt;Not surprising, but they removed the code of conduct. Spreading hate is far easier without one, I
get that.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;It’s pretty obvious that they’re building their own echo chamber. Full of far-right content, lots
and lots of video content and even though they took Mastodon as their codebase, they’re not
interested in federation. They probably just want to benefit from all the apps/clients that are
built for that.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Eure Prüderie kotzt mich an.</title>
    <link href="https://jasminchen.dev/notes/2024/pruederie-hetzner/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>44f1982ee2e38608d4f02192e0f3fb58cafb185b</id>
    <content type="html">&lt;p&gt;&lt;em&gt;Inhaltswarnung: Sex, vulgäre Sprache&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Hallo liebe Hetzner-Menschis,&lt;/p&gt;
&lt;p&gt;ich hab in den letzten zwei Tagen ein bisschen mehr mit euch geschrieben. Und eigentlich hab ich
auch immer sehr viel von euch gehalten.&lt;/p&gt;
&lt;p&gt;In den letzten fünf Jahren hatte ich nie ein technisches Problem mit eurer Infrastruktur. Im
Gegenteil: sie lief und lief und lief. So gesehen, müsste ich die glücklichste Kund*in sein.
Tatsächlich hatte ich mich sogar bei euch beworben, aber das ist dann leider nichts geworden.&lt;/p&gt;
&lt;p&gt;Aber in den letzten beiden Tagen habt ihr mich doch enttäuscht. Technisch ist alles weiterhin
bestens, daran liegt’s nicht. Eher so das menschliche, you know, eure allgemeinen
Geschäftsbedingungen und so. Vielleicht bin ich auch selber schuld, dass ich da im Voraus nicht
nachgefragt hab, wie eure AGB auszulegen sind.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Es ist richtig, dass pornografische Inhalte von Sexarbeitenden, auch wenn sie volljährig sind und
alle Inhalte mit Einverständnis für Produktion und Reproduktion erstellt wurden, gegen unsere AGB
verstoßen und daher auf unseren Servern nicht erwünscht sind. Die Entscheidung, bei dem Verbot
pornografischer Inhalte über die Wertungen des strafrechtlich erlaubten hinaus auch legale Formen
der Pornografie nicht zuzulassen ist eine Grundsatzentscheidung der Unternehmensleitung. Deshalb
sind unsere AGB insofern auch weitergehend als die gesetzlichen Regelungen. &lt;cite&gt;Zitat aus der
E-Mail der Hetzner-Rechtsabteilung&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Anyway. &lt;strong&gt;Eure Prüderie kotzt mich an.&lt;/strong&gt; Ich versteh ja, dass ihr bei Beschwerden von außen
reagieren müsst. Und sobald wir von der Darstellung Minderjähriger sprechen, sind die Regeln da auf
lokaler und internationaler Ebene sehr streng. Versteh ich. Aber Pornografie ist nun eben ein sehr
weites Spektrum. Erotik auch.
&lt;a href=&quot;https://netzpolitik.org/2023/was-ist-grob-aufdringlich-die-feine-linie-zwischen-porno-und-erotik/&quot;&gt;Kann so richtig auch niemand definieren&lt;/a&gt;.
Aber warum ist eure Geschäftsführung da so prüde? Warum habt ihr mit Pornografie so ein Problem? Ich
ahne es. Es ist fürs Image, ne? Damit neben super-mega-startup.de nicht gleich noch
tittenbericht24.com liegt. jaja, böse titten.&lt;/p&gt;
&lt;p&gt;Wisst ihr nur, warum das schade ist? Weil für mich als queere Person die Nacktheit und damit auch im
weiteren Sinne die Pornografie ein Akt der Selbstbestimmung ist. Es ist mein Körper, es sind meine
Bilder, alleine die Produktion dieser Bilder ist schon eine Auflehnung gegen die gesellschaftliche
Mehrheit. Gewissermaßen ein Akt der Rebellion. &lt;strong&gt;Das System fickt uns, also ficken wir zurück.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lasst mich doch meine Selbstbestimmung feiern. Ob nun im Bikini oder oberkörperfrei, solange keine
gesetzlichen Grenzen überschritten werden, seh ich keinen Handlungsbedarf. Aber naja. In einem
Internet, in dem jedes bisschen Nacktheit direkt zur Sperrung führt, reiht ihr euch leider nur mit
ein. Bin ich überrascht? Nicht wirklich. Aber enttäuscht.&lt;/p&gt;
&lt;p&gt;Ich hab nicht immer Lust, nur auf den Tag zu warten, wo ich auf der Abschlussliste stehe und dann
von irgendwelchen Meldesystemen vertrieben werde. Ich würd mich gern auch einfach mal so entfalten
dürfen. Brustwachstum statt Wirtschaftswachstum feiern.&lt;/p&gt;
&lt;p&gt;Werd ich auch. Aber dann wohl nicht mehr auf eurer Infrastruktur.&lt;/p&gt;
&lt;p&gt;Schade Schokolade.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Goodbye Arch, hello Fedora</title>
    <link href="https://jasminchen.dev/notes/2024/goodbye-arch/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>e2d38320e87ebda3460d570e861a32ddab801457</id>
    <content type="html">&lt;p&gt;How do you know that you’re getting older? Easy: installing updates is just another chore and does
not cause excitement anymore. And that’s the reason why I replaced Arch on both of my computers. My
current device, a ThinkPad T460 (with the lovely hostname &lt;em&gt;twinkpad&lt;/em&gt;) was the last device which was
running Arch.&lt;/p&gt;
&lt;h2 id=&quot;why-i-switched-away&quot;&gt;Why I switched away&lt;/h2&gt;
&lt;p&gt;Even though I used Arch for years now, I decided to switch. The primary reason is: I’m not using my
TP that often. Maybe once or twice a week. And with each usage, I ran the obligatory
&lt;code&gt;paru &amp;amp;&amp;amp; poweroff&lt;/code&gt; before I put it away. And that turned out to be a larger chore over the last
couple of weeks. Reviewing the &lt;code&gt;PKGBUILD&lt;/code&gt; files, waiting for the packages to compile (even tho I
tried to use the &lt;code&gt;*-bin&lt;/code&gt; packages wherever possible), I got annoyed by almost everything.&lt;/p&gt;
&lt;p&gt;And so I decided to replace the operating system. Since my
&lt;a href=&quot;https://github.com/nachtjasmin/dotfiles/&quot;&gt;dotfiles are already managed via Git&lt;/a&gt;, I did not have too
many files to backup. Just the documents, some downloads and the repositories. Most of the stuff was
backed up anyway, but eh, better be safe. &lt;code&gt;rsync&lt;/code&gt; was a massive help here.&lt;/p&gt;
&lt;h2 id=&quot;hello-there-fedora-kinoite-3&quot;&gt;Hello there, Fedora Kinoite :3&lt;/h2&gt;
&lt;p&gt;For my next system, I chose &lt;a href=&quot;https://fedoraproject.org/atomic-desktops/kinoite/&quot;&gt;Fedora Kinoite&lt;/a&gt;.
There are multiple reasons for that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I’m already familiar with Fedora. Fedora Workstation is installed on my work laptop, Fedora
Server on my homeserver.&lt;/li&gt;
&lt;li&gt;It’s immutable and still easy to understand.&lt;/li&gt;
&lt;li&gt;KDE. ❤️&lt;/li&gt;
&lt;li&gt;✨ shiny new stuff ✨&lt;/li&gt;
&lt;li&gt;hopefully less work with updates.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I could’ve chosen Fedora Workstation, but I really wanted to explore the “immutable” part of it.
“Immutable” might not be the correct term, indeed,
&lt;a href=&quot;https://blog.verbum.org/2020/08/22/immutable-%E2%86%92-reprovisionable-anti-hysteresis/&quot;&gt;“image based” might be better&lt;/a&gt;.
I still have my mutable filesystems. But the system and all it’s packages are no longer mutable by
default. This is different from my existing devices which all use a “classic” mutable operating
system. I really wanted to try out NixOS, but to be honest: learning Nix to manage my system is out
of my scope. Maybe if I have enough time, I &lt;em&gt;might&lt;/em&gt; look again into it.&lt;/p&gt;
&lt;p&gt;But the biggest chore, the software updates, are a breeze with that setup. I can use &lt;em&gt;Discover&lt;/em&gt;, the
software manager of KDE to update the whole system with one click and it’s automatically applied on
the next reboot. And even if something breaks, I can still access the previous state and therefore
(at least in theory) always end up with a working system.&lt;/p&gt;
&lt;h2 id=&quot;using-toolbx&quot;&gt;Using toolbx&lt;/h2&gt;
&lt;p&gt;The recommended way on immutable systems like Fedora Kinoite is to use
&lt;a href=&quot;https://github.com/containers/toolbox&quot;&gt;toolbox&lt;/a&gt;. For those who are unfamiliar with that, I’ll quote
a part of their project description:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Toolbx environments have seamless access to the user’s home directory, the Wayland and X11
sockets, networking (including Avahi), removable devices (like USB sticks), systemd journal, SSH
agent, D-Bus, ulimits, /dev and the udev database, etc…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And so I was exploring with that as well, only to realize pretty quickly that I want a custom
container image for that. For example, I use &lt;a href=&quot;https://fishshell.com/&quot;&gt;fish&lt;/a&gt; as my primary shell
instead of bash, &lt;a href=&quot;https://sw.kovidgoyal.net/kitty&quot;&gt;kitty&lt;/a&gt; as my terminal emulator (and therefore
&lt;code&gt;kitty-terminfo&lt;/code&gt; should be installed) and so on. That’s how my
&lt;a href=&quot;https://github.com/nachtjasmin/toolbox&quot;&gt;custom container image&lt;/a&gt; was born. I
&lt;a href=&quot;https://github.com/nachtjasmin/dotfiles/commit/56dfb01b1324f0213181bea69b09c9e8b1257a21&quot;&gt;also configured toolbox&lt;/a&gt;
to automatically use this image for new containers.&lt;/p&gt;
&lt;p&gt;I’m not sure whether I want to setup a toolbox environment per project (maybe in combination with
&lt;a href=&quot;https://direnv.net/&quot;&gt;direnv&lt;/a&gt;) or if I want to use one container for everything. Only time can tell,
I guess. 😄&lt;/p&gt;
&lt;h2 id=&quot;the-future&quot;&gt;The future&lt;/h2&gt;
&lt;p&gt;I’m definitely curious about the long-term simplicity of this setup. If everything goes well, this
setup might last for years with little to none maintenance work. If not, well, then I’ll replace the
OS again. easy (and annoying) as that. ^^&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Jasmin wrapped</title>
    <link href="https://jasminchen.dev/notes/2023/jasmin-wrapped/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>12f5de55b823b7251d04fcaa531bc0aef7de01c3</id>
    <content type="html">&lt;p&gt;Inspiriert von &lt;a href=&quot;https://jascha.wtf&quot;&gt;Jascha&lt;/a&gt;, dachte ich mir, jo, Jahresrückblick dieses Jahr wäre
mal nice. Ich hab für sowas in der Vergangenheit halt einfach die sozialen Medien (sprich damals
Twitter, heute das Fediverse) genutzt, aber doch realisiert, dass dort einiges einfach untergeht.
Und daher, mach ich das diesmal hier.&lt;/p&gt;
&lt;p&gt;Zusammenfassend war dieses Jahr wirklich nur von Turbulenzen geprägt. Ich bin das zu einem gewissen
Grad durchaus gewohnt und komme relativ gut damit klar, nur dieses Jahr ist es wirklich sehr viel
gewesen. Es sind sehr viele gute Dinge passiert und ich für meinen Teil bin insbesondere mit der
Entwicklung meiner Persönlichkeit sehr zufrieden. Die Therapie, die dann in diesem Jahr nach zwei
Jahren ihr Ende nahm und mein Rückblick auf einige meiner getätigten Postings im Vergangenheit haben
mir gezeigt, dass ich da sehr gut Fortschritte gemacht habe.&lt;/p&gt;
&lt;h2 id=&quot;haare-laenger-oder-kuerzer&quot;&gt;Haare länger oder kürzer?&lt;/h2&gt;
&lt;p&gt;Kürzer. Undercut for the underdog. Oder so. Es tut auf jeden Fall gut, mehr Androgynität im Stil zu
haben.&lt;/p&gt;
&lt;h2 id=&quot;haare-gefaerbt&quot;&gt;Haare gefärbt?&lt;/h2&gt;
&lt;p&gt;Ich glaub, drei mal. Am besten gefallen hat mir das floureszierende Neongrün, welches ich auch auf
der GPN getragen habe. Leuchtende Haare sind einfach bombe, das wird 2024 definitiv wiederholt.&lt;/p&gt;
&lt;h2 id=&quot;mehr-kohle-oder-weniger&quot;&gt;Mehr Kohle oder weniger?&lt;/h2&gt;
&lt;p&gt;Neuer Job, neues Glück. Ich habe dem öffentlichen Dienst den Rücken zugewandt und einen Job
gefunden, der genügend Geld einbringt, um in der Theorie drei Menschen zu versorgen.&lt;/p&gt;
&lt;h2 id=&quot;mehr-ausgegeben-oder-weniger&quot;&gt;Mehr ausgegeben oder weniger?&lt;/h2&gt;
&lt;p&gt;Kann ich nicht sagen, ich führe keine Buchhaltung. Aber immerhin ist der Stromverbrauch dieses Jahr
gesunken. Auf der Liste des Erwachsenseins ist das wohl eines der Dinge, über das ich mich echt
freue.&lt;/p&gt;
&lt;p&gt;Vermutlich aber durchaus mehr Geld ausgegeben, 2023 wurden nicht selten die Haare gefärbt. Von den
drei Piercings im Ohr mal ganz abgesehen. Ups.&lt;/p&gt;
&lt;h2 id=&quot;mehr-bewegt-oder-weniger&quot;&gt;Mehr bewegt oder weniger?&lt;/h2&gt;
&lt;p&gt;Leider deutlich weniger. Ich hatte in 2022 wenigstens noch regelmäßig einen Spaziergang drin, das
ist 2023 deutlich weniger geworden. Homeoffice in Vollzeit trägt sein übriges dazu bei, mein iPhone
berichtet mir so im Schnitt an die 500 Schritte täglich.&lt;/p&gt;
&lt;p&gt;Das ist viel zu wenig, daher steht für 2024 deutlich mehr Bewegung auf dem Plan. Werde mich wohl mal
wieder häufiger aufs Rad schwingen.&lt;/p&gt;
&lt;h2 id=&quot;der-hirnrissigste-plan&quot;&gt;Der hirnrissigste Plan?&lt;/h2&gt;
&lt;p&gt;Meine Verlobte dazu ermutigen, den bisherigen Job und damit das gesamte Haushaltseinkommen an den
Nagel zu hängen. Rückblickednd war es eine sehr gute Entscheidung, nur eben auch mit sehr viel
Risiko verbunden.&lt;/p&gt;
&lt;h2 id=&quot;die-gefaehrlichste-unternehmung&quot;&gt;Die gefährlichste Unternehmung?&lt;/h2&gt;
&lt;p&gt;2023 war komplett ohne gefährliche Unternehmungen und wenn es nach mir geht, darf das genau so
bleiben.&lt;/p&gt;
&lt;h2 id=&quot;die-teuerste-anschaffung&quot;&gt;Die teuerste Anschaffung?&lt;/h2&gt;
&lt;p&gt;Ich bin in der Regel sehr zurückhaltend was Anschaffungen angeht. Vermutlich ändert sich auch das
nächstes Jahr, da plane ich mal, den Kleiderschrank auszumisten und zu erweitern.&lt;/p&gt;
&lt;h2 id=&quot;das-beeindruckendste-buch&quot;&gt;Das beeindruckendste Buch?&lt;/h2&gt;
&lt;p&gt;Ich bin es noch am Lesen, da ich dieses Jahr leider nicht viel gelesen hab. Aber &lt;em&gt;Identitätskrise&lt;/em&gt;
von Alice Hasters dürfte mein Favorit des Jahres werden.&lt;/p&gt;
&lt;h2 id=&quot;der-ergreifendste-film&quot;&gt;Der ergreifendste Film?&lt;/h2&gt;
&lt;p&gt;Auch da führe ich keine Statistik, weiß aber, dass ich vermutlich bei &lt;em&gt;Elemental&lt;/em&gt; mehrfach pausieren
und heulen musste. Aus Gründen.&lt;/p&gt;
&lt;h2 id=&quot;die-meiste-zeit-verbracht-mit&quot;&gt;Die meiste Zeit verbracht mit …?&lt;/h2&gt;
&lt;p&gt;Meinen Verlobten bzw. meiner Verlobten und meiner Ex-Verlobten.&lt;/p&gt;
&lt;h2 id=&quot;die-schoenste-zeit-verbracht-mit&quot;&gt;Die schönste Zeit verbracht mit …?&lt;/h2&gt;
&lt;p&gt;Meinen Verlobten bzw. meiner Verlobten und meiner Ex-Verlobten.&lt;/p&gt;
&lt;h2 id=&quot;vorherrschendes-gefuehl-2023&quot;&gt;Vorherrschendes Gefühl 2023?&lt;/h2&gt;
&lt;p&gt;Uff. Schwere Frage. Ich denke: Zuversicht. Insgesamt ist es doch die Zuversicht, die nach der
Depression zurückkehrte.&lt;/p&gt;
&lt;h2 id=&quot;2023-zum-ersten-mal-getan&quot;&gt;2023 zum ersten Mal getan?&lt;/h2&gt;
&lt;p&gt;Eine Beziehung beenden müssen. Leider.&lt;/p&gt;
&lt;h2 id=&quot;2023-nach-langer-zeit-wieder-getan&quot;&gt;2023 nach langer Zeit wieder getan?&lt;/h2&gt;
&lt;p&gt;Mich unter Menschen begeben. Das waren in 2023 die Easterhegg, die GPN und die MRMCD und zum
Jahresabschluss die Weihnachtsfeier auf der Arbeit.&lt;/p&gt;
&lt;p&gt;Das hat jedoch auch sehr viele Nerven gekostet. Insbesondere musste ich lernen, wann und wie:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;die Überreizung stattfindet,&lt;/li&gt;
&lt;li&gt;ich entsprechend meine Bedürfnisse kommunizieren kann,&lt;/li&gt;
&lt;li&gt;und all dies auch rechtzeitig zu erkennen.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Die Weihnachtsfeier mit den knapp 400 Menschen und der lauten Musik war somit auch ein wenig der
Stresstest (quite literally), wie gut ich damit klarkomme. Und im Gegensatz zu den Chaos-Events
davor auch ohne Begleitung in Form meiner (Ex)-Partner*innen. Ich habe ehrlich gesagt nicht damit
gerechnet, insgesamt so gut zurecht zu kommen, auch wenn ich an der Früherkennung für Überreizung
noch arbeiten muss.&lt;/p&gt;
&lt;p&gt;Aber für den Fall hab ich inzwischen auch immer ein paar Ohrstöpsel dabei, das sollte beim nächsten
Mal helfen. :3&lt;/p&gt;
&lt;h2 id=&quot;drei-dinge-auf-die-ich-gut-haette-verzichten-koennen&quot;&gt;Drei Dinge, auf die ich gut hätte verzichten können?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Das abrupte Ende der Beziehungen.&lt;/li&gt;
&lt;li&gt;Die COVID-Infektionen, die ich gerne vermieden hätte, aber nun, iat halt wirklich ungünstig
gelaufen.&lt;/li&gt;
&lt;li&gt;Der Stress, den die Jobsuche so mit sich bringt. Ich bin für die unehrliche Selbstvermarktung
nicht geschaffen.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;das-schoenste-geschenk-das-mir-jemand-gemacht-hat&quot;&gt;Das schönste Geschenk, das mir jemand gemacht hat?&lt;/h2&gt;
&lt;p&gt;Es gibt nicht das schönste Geschenk für mich. Das größte Geschenk, und ich weiß wie kitschig das
klingt, sind meine Partner*in und Freund*innen. Und ganz egal ob es die Behausung oder eine
Umarmung ist, ich bin dankbar für all die Liebe und gemeinsame Zeit, die ich mit euch verbringen
darf. 💜&lt;/p&gt;
&lt;h2 id=&quot;ueberraschendstes-ereignis&quot;&gt;Überraschendstes Ereignis?&lt;/h2&gt;
&lt;p&gt;Die &lt;a href=&quot;https://chaosdorf.de/2023/11/awareness-team-gewaehlt/&quot;&gt;Wahl des Awareness-Teams&lt;/a&gt;. Ich hab
ehrlich nicht damit gerechnet, dass es einerseits diese Zustimmung bekommen würde und zeitgleich
auch mir als Person so viel Vertrauen entgegengebracht wird.&lt;/p&gt;
&lt;p&gt;Und ohne zu sehr ins Detail zu gehen: es war anscheinend auch bitter nötig.&lt;/p&gt;
&lt;h2 id=&quot;2023-war-mit-einem-wort&quot;&gt;2023 war mit einem Wort… ?&lt;/h2&gt;
&lt;p&gt;Kräftezehrend.&lt;/p&gt;
&lt;h2 id=&quot;plaene-fuer-2024&quot;&gt;Pläne für 2024?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Heiraten. Aus Liebe &lt;em&gt;und&lt;/em&gt; steuerlichen Gründen.&lt;/li&gt;
&lt;li&gt;Mehr, beziehungsweise überhaupt mal wieder Rad fahren bzw. sportlich betätigen.&lt;/li&gt;
&lt;li&gt;Den noch ausstehenden Konflikt mit meiner Ex-Partnerin abschließen.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;besonders-dankbar-fuer&quot;&gt;Besonders dankbar für…&lt;/h2&gt;
&lt;p&gt;Wie eben schon erwähnt, bin ich allgemein sehr dankbar für alle meine Freund*innen. Dennoch möchte
ich einen besonderen Dank aussprechen, an (Sortierung alphabetisch):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;derf&lt;/em&gt;: Wir haben dieses Jahr einiges an Zeit verbringen dürfen, sei es im Chaosdorf oder beim
gemeinsamen Waffeln futtern. Du bist eine super liebe Freundin und ich bin sehr dankbar, dass du
mich das ein oder andere Mal dann doch liebevoll zurückgepfiffen hast, wenn ich mal wieder über
die Stränge gesprungen bin.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Lari&lt;/em&gt;: Unsere langjährige Beziehung wär dieses Jahr fast zersprungen. Ich bin dir unglaublich
dankbar für das entgegengebrachte Vertrauen, für die Unterstützung und all die gemeinsamen
Momente. Und ich bin sehr zuversichtlich, dass die kommenden Jahre weniger stressig werden, als es
2023 noch war.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;maximemelian&lt;/em&gt;: Du warst für mich da, als ich es am Dringendsten brauchte. &lt;strong&gt;Danke.&lt;/strong&gt; Ich werde
dir das nie vergessen. Und natürlich bist du abseits davon auch eine absolut wunderbare Person und
ich habe jeden einzelnen Moment mit dir genossen.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Tessa&lt;/em&gt;: Auch wenn unsere Beziehung in 2023 begonnen hat und wieder endete: &lt;strong&gt;Danke für alles.&lt;/strong&gt;
Jeder einzelne Moment mit dir war wunderschön. Ich wünschte rückblickend, dass ich dir einen Teil
der Achterbahn hätte ersparen können, aber ich hab leider erst durch die Beziehung mit dir
gelernt, dass Fernbeziehungen nichts für mich sind. Tut mir leid. Ich freu mich schon auf all die
Momente, die wir in 2024 dann verbringen können.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Vivien&lt;/em&gt;: Leider gab es zwischen uns im vergangenen Jahr sehr viele Konflikte, von denen ich mir
wünschte, dass sie nicht existiert hätten. Aber wir haben auch sehr viele schöne Momente gehabt,
an die ich sehr gerne zurückdenke. Und für die möchte ich mich bei dir bedanken. 2024 wird
bestimmt auch wieder welche davon mit sich bringen, die Zuversicht meinerseits besteht zumindest.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>The promise of the fediverse hasn&#39;t been fulfilled yet</title>
    <link href="https://jasminchen.dev/notes/2023/the-promise-of-the-fediverse-hasnt-been-fulfilled-yet/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>ff9e8badfef3d4eea02276027a8a755ca5973daf</id>
    <content type="html">&lt;p&gt;After thinking about &lt;a href=&quot;https://queer.group/@jasmin/111649632136007927&quot;&gt;starting yet another project&lt;/a&gt;
(as if I don’t have enough ideas already) and what could be improved, I fell into a rabbit hole of
ideas. This blog post is the result of that.&lt;/p&gt;
&lt;h2 id=&quot;which-promise-jasmin&quot;&gt;Which promise, Jasmin?&lt;/h2&gt;
&lt;p&gt;What do I mean with “the promise of the fediverse”? Basically it’s the idea that because of
ActivityPub and interoperability, we can now each use a different software to interact with others.
If you are on Mastodon, you can follow your friends on Pixelfed, know what they’re reading on
Bookwyrm and so on. You get the idea.&lt;/p&gt;
&lt;p&gt;The thing is: social media on the fediverse is dominated by Mastodon. Which itself isn’t that bad,
in theory, it’s just a simple software which could be replaced with something better. And we have
ActivityPub, so all those interactions from Misskey, GoToSocial, are still visible to Mastodon users
and vice-versa. Unfortunately, this leads to one thing: literally &lt;strong&gt;everything&lt;/strong&gt; has to be
compatible with Mastodon. Not because it’d be necessary, but Mastodon itself is seen as &lt;em&gt;the&lt;/em&gt;
fediverse and therefore it’s expected that I can interact with everything from my Mastodon account.
(On a technical note, everything ends up being a &lt;code&gt;Note&lt;/code&gt; in ActivityPub.)&lt;/p&gt;
&lt;h2 id=&quot;why-social-media-doesn-t-feel-social-anymore&quot;&gt;Why social media doesn’t feel &lt;em&gt;social&lt;/em&gt; anymore&lt;/h2&gt;
&lt;p&gt;And to be honest: this isn’t &lt;em&gt;social&lt;/em&gt;. &lt;em&gt;Social media&lt;/em&gt;, as we collectively name the different
platforms, became somewhat “antisocial”. Mastodon itself is just Twitter, decentralised. The idea of
Twitter itself is “create an account, connect with others (friends, corporations) and tweet
sometimes”. It was fun back then. Limited in expression, due to the 140 character limit. Later that
got expanded to 280 chars and that was maybe the best thing that could’ve happened, at least to me.
I found a lot of friends, I could connect with others without promoting myself all the time. I was
just a username, a profile, and a short bio. I still love that idea and I do get why Mastodon copied
it.&lt;/p&gt;
&lt;p&gt;The fediverse just lacks one thing: corporations. For me and others, this is a selling point. No
ads, no shady business scams. (Well, they do exist, but as an admin, I can block them) But Mastodon
itself is built on the idea that corporations &lt;em&gt;might&lt;/em&gt; use it one day. And therefore even the fun
things like the “toot” button got lost. The color scheme is no longer blue, but &lt;em&gt;blurple&lt;/em&gt;. I kid you
not, that’s the
&lt;a href=&quot;https://github.com/mastodon/mastodon/blob/a2624ff739ea555888f27b13a44f7d6b1af35a86/app/javascript/styles/mastodon/variables.scss&quot;&gt;“official” name inside the source code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything is blurple or “neutral” today.&lt;/strong&gt; Discord and Mastodon are blue-purple-ish, Twitter is
just black and white (if fascism would be a color, Twitter would use it) and the rest of the
internet has the same corporate color scheme. Sometimes the changes are good, like for the Wikipedia
redesign in 2022, but most of the time, I just feel disconnected. The thing where the internet
nowadays fails: &lt;strong&gt;connecting with people and friends.&lt;/strong&gt; Nowadays there’s just &lt;code&gt;@handle@platform.tld&lt;/code&gt;
as a way to use social media. This doesn’t feel right. I am one body, but I have a lot of personas
(well, I’m plural, but that doesn’t matter here). For some I am Jasmin, their coworker, for others,
I’m Jasmin the good friend and for others I’m the kinky trans femme from the neighbourhood. On
Mastodon, and I use it as an example, because I use it, I’m all of that at the same time. I cannot
choose how to interact with others and what others should see of me. In terms of plurality, I cannot
even properly let the other headmates front. &lt;strong&gt;That sucks.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;social-media-in-the-past&quot;&gt;Social media in the past&lt;/h2&gt;
&lt;p&gt;I am 25 years old. I never used Myspace and for some reason, it still exists after 20 years. No one
I know uses it. I used Facebook in the past, as it was the lowest common denominator to exchange
homework back then. I started using Twitter in the beginning of 2013. There’s even a forum post
(that I won’t link to, cause it contains my deadname) from January 6th, 2013 where I asked why
Twitter was so popular. I didn’t get the idea back then. We all know what happened to Facebook: it
became the social media platform for boomers, right-wing extremists and so on. Renaming the platform
to “Meta” and imagining the “Metaverse” as some kind of VRChat without furries didn’t help avoiding
the collapse.&lt;/p&gt;
&lt;p&gt;But I have to admit, Facebook did one thing good: You could actually connect with friends. You could
chat with them, plan events, exchange stuff in groups. To my knowledge, no other social media
platform has tried to fill that gap. Facebook is not cool, so you don’t copy it. Of course, because
of ads and stuff you could do a lot of other stuff as well, like playing little flash games and
competing with your friends. (Nowadays, Discord fills in that gap.)&lt;/p&gt;
&lt;h2 id=&quot;post-privacy-options-and-google&quot;&gt;Post privacy options and Google+&lt;/h2&gt;
&lt;p&gt;Then there was Google+. I used it and do you know what? It was the first place in the web where I
felt home. Facebook always felt like a competition (who has the most friends, who changed their
relationship status and when) and on Twitter I felt like a stranger. I was just there, shy and not
knowing where I belong. Google+ only had some big corporate accounts and “tech influencers” as I
like to call it, but the idea of &lt;em&gt;circles&lt;/em&gt; never came back. The idea is simple: instead of having
just one profile, you could decide how others perceive you. You could post to the “friends” circle
that you want to hangout, immediately followed up by a post to the “wider friends” circle that you
like the weather today. Or something like that.&lt;/p&gt;
&lt;p&gt;Today, at least on Mastodon, you only have four options for the visibility of your posts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Public: everyone can read it.&lt;/li&gt;
&lt;li&gt;Unlisted: everyone can read it, it just doesn’t appear in the federated or local timeline.&lt;/li&gt;
&lt;li&gt;Followers-only: only people who follow you or those who have been mentioned can read it.&lt;/li&gt;
&lt;li&gt;Direct: only mentioned people can read it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That doesn’t reflect any of the personas I mentioned earlier. For example, if I wanna post nudes on
the fediverse, I can safely do that. But &lt;em&gt;I&lt;/em&gt; cannot exclude that stuff from minors, even if I want
to. Either I create a second NSFW-only account or I just don’t post it and hope it’ll be handled by
them properly. But I know how I felt as a minor on the web and I loved it when others interacted
with me, because I felt valued. Seen as a person and not treated like a child. I tend to forget
that, and
&lt;a href=&quot;https://blog.hellbeast.eu.org/hellbeast.eu.org/Published/Fedi%20fucking%20sucks%20(as%20a%20minor)&quot;&gt;this blog post from hellbeast&lt;/a&gt;,
where pup described pups own experience, reminded me of my own.&lt;/p&gt;
&lt;p&gt;I think we all know toots/posts/images etc. that we’re not comfortable to share with one group or
another for whatever reason, nudity and sexual imagery is just one of them.&lt;/p&gt;
&lt;h2 id=&quot;other-things-that-suck-right-now&quot;&gt;Other things that suck right now&lt;/h2&gt;
&lt;h3 id=&quot;the-good-old-calendar-scheduling-problem&quot;&gt;The good old calendar scheduling problem&lt;/h3&gt;
&lt;p&gt;I miss the Microsoft Exchange functionality in Mastodon. I am not joking. Have you tried to meet
with friends in 2023? Either you all have their phone numbers or you decide on a chat platform,
which usually is Discord or Matrix in my circles, to collectively decide on a date. And everyone has
to put it in their own calendar. The &lt;a href=&quot;https://en.wikipedia.org/wiki/ICalendar&quot;&gt;.ics format&lt;/a&gt; is as
old as me, come on, at least a little calendar subscription should be doable.&lt;/p&gt;
&lt;p&gt;I can plan events in Discord, I could do that in Facebook back then. I cannot do it with a fediverse
software right now. Even if I’d just use it for events like the
&lt;a href=&quot;https://events.ccc.de/congress/2023/infos/index.html&quot;&gt;37c3&lt;/a&gt; in Hamburg, a little meet up of
friends, whatever, I miss that. In the microblogging world, things like that are out of scope, for a
good reason. It just has nothing to do with blogging. I can write “I’m at the grass at GPN” and
that’s one way to connect, but if wanna meet up for a monthly potluck or something like that, I have
to coordinate that somewhere else.&lt;/p&gt;
&lt;h3 id=&quot;moderation-and-federation&quot;&gt;Moderation and federation&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Disclaimer: I’m administrator and also the sole moderator of queer.group. I can only speak from my
experience as the admin, I haven’t experienced that topic purely from an users perspective.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you are not aware with federation, let’s provide a quick glance at what’s possible with Mastodon
right now:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Silencing an instance.&lt;/strong&gt; This has the effect that posts from there won’t be visible to you,
unless you follow the respective accounts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suspending an instance.&lt;/strong&gt; Communication is impossible now, they won’t see your posts (with
&lt;code&gt;AUTHORIZED_FETCH&lt;/code&gt; turned on) and you won’t see theirs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I could write a blog post about why the federation model is good and bad at the same time, but eh,
not today. I’ll try to focus on the actual effect of these options for now. &lt;strong&gt;And turns out, both
options suck.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Silencing sucks, because half of the timeline suddenly is not being fetched/rendered (in case you
open a thread for example) and you don’t even have the “Load more” button like e.g. on Twitter. You
know the stuff is there, you just need to open it on their web page. In the best case, you can read
the thread and be happy, in the worst case you also see the other problematic instances and might
have a bad day then.&lt;/p&gt;
&lt;p&gt;Suspending is better, because at least it doesn’t hide things from you or suggests something to
exist, nope, it’s completely erased from the instance. At least it feels like that. The problem
occurs with bigger instances, where I as the moderator have to choose between cutting an enormous
amount of connections or to protect minorities from racist/ableist/… attacks. Personally, I tend
to choose the latter, as it’s possible for the so-called &lt;em&gt;good&lt;/em&gt; accounts to switch their instance.&lt;/p&gt;
&lt;h3 id=&quot;account-portability-and-the-ephemeral-nature-of-posts&quot;&gt;Account portability and the ephemeral nature of posts&lt;/h3&gt;
&lt;p&gt;At least in theory. Practically, switching an instance is not that easy. There are migration tools
inside Mastodon and in theory, you won’t lose the relationships to the accounts. But all your posts,
pics, etc. they won’t get moved. &lt;em&gt;Personally&lt;/em&gt;, I don’t mind. “We” (as mods) tell others that their
posts are ephemeral, they shouldn’t treat the Mastodon instance like some kind of archive. It was
never made for that.&lt;/p&gt;
&lt;p&gt;But: We all use social media platforms in a different way. Maybe you appreciate exactly that part of
posting, maybe you use the fediverse as a diary. Sure, I can say “start a blog”, but honestly: I
cannot expect others to start a blog when I complain that the web got so hard to understand at the
same time. Back then, when I tried to setup a WordPress instance (10 years ago or something like
that) it was just:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;extract this zip folder&lt;/li&gt;
&lt;li&gt;open FileZilla&lt;/li&gt;
&lt;li&gt;copy it to your host&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Theoretically, WordPress still can be installed this way,
&lt;a href=&quot;https://smitka.me/2022/07/01/wordpress-installer-attack-race/&quot;&gt;you just need to be quick enough with the setup&lt;/a&gt;.
“Host your own fediverse instance” - oh boy, I wouldn’t recommend that. Sure, doing fun with
computers can be fun, but you need to learn a lot of concepts. Install this, install that, ensure a
firewall for this, create backups, and so on. It’s too complicated. &lt;strong&gt;You naturally need to rely on
someone else to do this stuff for you.&lt;/strong&gt; That’s just how the society works. Everyone in this little
society has their abilities and skills and we help and support each other. I do computer stuff, you
can do woodworking and together we help the disabled person who can’t do any of those things. That’s
how I imagine a healthy society. (Side note: healthy in the sense of collaboration, not in some
eugenic way)&lt;/p&gt;
&lt;p&gt;Back to the topic of account portability: There’s no way of moving the account to a different
instance right now. It’s not even possible to switch the domain, for example from &lt;code&gt;exampl.com&lt;/code&gt; to
&lt;code&gt;example.com&lt;/code&gt; without setting up a new instance. And it doesn’t has to be this way.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Instances don’t technically exist:
https://www.w3.org/TR/activitypub/#server-to-server-interactions –
&lt;a href=&quot;https://hachyderm.io/@thisismissem/111654995766322845&quot;&gt;ThisIsMissEm&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We do not have to focus around instances. Mastodon decided to do so and everyone else followed.
Likely it’s not even possible to revert some of the decisions that were made back then.&lt;/p&gt;
&lt;h2 id=&quot;so-what-do-you-want-jasmin&quot;&gt;So, what do you want, Jasmin?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Maoam#Advertisements&quot;&gt;Maoam&lt;/a&gt;. Jokes aside, I want a &lt;em&gt;social&lt;/em&gt; media
platform (everything’s a platform nowadays, but that’s a rant for another day) which is built around
people, not accounts. I want to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Connect with my friends, knowing what they’re up to.&lt;/li&gt;
&lt;li&gt;Want to post my silly thoughts into the fediverse&lt;/li&gt;
&lt;li&gt;Want to connect in smaller groups (where everything I post is only visible to that group)&lt;/li&gt;
&lt;li&gt;Chat with people occasionally without the need to switch to Discord, Matrix or something else.
For things like “hey @ &amp;lt;10 people&amp;gt;, wanna meet at 9pm in &lt;location&gt;” and stuff like that.&lt;/location&gt;&lt;/li&gt;
&lt;li&gt;Schedule events and see what’s going on in my region&lt;/li&gt;
&lt;li&gt;Use different personas for different things, even if connected to the same account, basically
&lt;a href=&quot;https://xeiaso.net/blog/plurality-driven-development-2019-08-04/&quot;&gt;plurality-driven development&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Essentially, I want to get away from instances as part of your online identity. I’m &lt;em&gt;Jasmin&lt;/em&gt;, not
&lt;code&gt;@jasmin@queer.group&lt;/code&gt;. The instance should matter for moderation stuff, but it shouldn’t be part
of the identity.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;so-you-just-want-facebook-but-federated&quot;&gt;So, you just want Facebook, but federated?&lt;/h2&gt;
&lt;p&gt;Probably, yes.&lt;/p&gt;
&lt;h2 id=&quot;is-this-even-possible&quot;&gt;Is this even possible?&lt;/h2&gt;
&lt;p&gt;I haven’t looked enough into the ActivityPub spec into what’s possible and what’s not possible.
Maybe there are limitations, but if there are any, they probably can also be removed. We, as
creatures on this planet, are more than a collection of &lt;code&gt;Note&lt;/code&gt; objects. The software should be built
around us.&lt;/p&gt;
&lt;h2 id=&quot;what-about-the-safety-of-minorities&quot;&gt;What about the safety of minorities?&lt;/h2&gt;
&lt;p&gt;I’d actually love this idea to be “allow-list by default”. Right now, if you open up an instance, it
just federates with everyone. Nazis, TERFs, all kind of douchebags. Tools, like
&lt;a href=&quot;https://thebad.space/&quot;&gt;thebad.space&lt;/a&gt; exist to keep the worst actors out and yet: if you don’t
actively moderate your instance, you’ll end up with a bunch of threatening people in your mentions,
it’s just a matter of time.&lt;/p&gt;
&lt;p&gt;If the approach would be inverted and therefore limited by default, this problem could be avoided,
maybe coming with a bunch of a lot of other problems. As I said, this whole blog post is just a
collection of thoughts.&lt;/p&gt;
&lt;p&gt;But: what if we take the concept of mutual trust and apply it to the fediverse? What if I could add
you as a friend and with a little “I trust you” checkmark, I just “subscribe” to your list of
allowed accounts/instances? I don’t need to be friends with any of them, but when I used Twitter, I
found this information (“&lt;em&gt;X&lt;/em&gt; and 37 others of people you follow, also follow &lt;em&gt;Y&lt;/em&gt;”) very valuable. We
also used &lt;em&gt;BlockTogether&lt;/em&gt; there, to effectively subscribe to the list of blocked accounts by our
friends. Even tho Twitter could’ve been a shithole, we built our community there. We took care of
each other as good as possible.&lt;/p&gt;
&lt;p&gt;The fediverse isn’t there yet.&lt;/p&gt;
&lt;h2 id=&quot;update-2023-12-29&quot;&gt;Update (2023-12-29)&lt;/h2&gt;
&lt;p&gt;After posting this blog post to the fediverse, I got a lot of feedback. And maybe, the little
project called &lt;a href=&quot;https://github.com/Letterbook/Letterbook&quot;&gt;Letterbook&lt;/a&gt; is going to be the Fediverse
solution I desperately seek. Thanks to Jennifer for the exchange!&lt;/p&gt;
&lt;p&gt;I cannot promise anything, but I plan to contribute to the project as well. :3&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Mastodon on Debian 12: libssl.so.1.1: cannot open shared object file</title>
    <link href="https://jasminchen.dev/notes/2023/updated-queer-group-to-debian-12/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>ac2b4e38f7f789f3fe1d1ac583e6e5e3374d6119</id>
    <content type="html">&lt;p&gt;Recently, I’ve migrated &lt;a href=&quot;https://queer.group&quot;&gt;our Mastodon instance&lt;/a&gt; from Debian 11 to Debian 12.
Although it wasn’t necessary, because Debian 11 still gets a lot of security updates, I wanted to do
it. New software always good.&lt;/p&gt;
&lt;p&gt;And everything worked like a charm. I just followed the
&lt;a href=&quot;http://web.archive.org/web/20250725213209/https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.en.html&quot;&gt;Debian upgrade guide&lt;/a&gt;,
restarted the server and got happy that Mastodon was still working like a charm.&lt;/p&gt;
&lt;p&gt;Well, libretranslate, which we’ve installed via pip, was no longer working due to
&lt;a href=&quot;https://peps.python.org/pep-0668/&quot;&gt;PEP-668&lt;/a&gt;, but that was mentioned in the release notes, so I was
prepared.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Debian provided python3 interpreter packages (python3.11 and pypy3) are now marked as being
externally-managed, following PEP-668. The version of python3-pip provided in Debian follows this,
and will refuse to manually install packages on Debian’s python interpreters, unless the
–break-system-packages option is specified.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, everything went smoothly? Nope.&lt;/p&gt;
&lt;h2 id=&quot;meet-dynamic-linking-my-beloved&quot;&gt;Meet dynamic linking, my beloved.&lt;/h2&gt;
&lt;p&gt;On Saturday, I got alerted by the monitoring system. The weekly scheduled task to clean up the
Mastodon media usage was failing with the following message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;libssl.so.1.1: cannot open shared object file&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The funny thing: the file existed. So, I guess, something something kernel update or so. Tried
several solutions, including &lt;code&gt;bundle install --redownload&lt;/code&gt;. Which presented me with even funnier
error messages.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Bundler::GemNotFound: Could not find rake-13.0.6.gem for installation

# ... (omitted for brevity)

An error occurred while installing rake (13.0.6), and Bundler cannot continue.

In Gemfile:
devise-two-factor was resolved to 4.0.2, which depends on
    devise was resolved to 4.8.1, which depends on
        responders was resolved to 3.0.1, which depends on
            railties was resolved to 6.1.7.4, which depends on
                rake
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point I was just like: “yeah, love you too. &lt;em&gt;deletes your vendor folder&lt;/em&gt;” And that actually
was the solution:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;su&lt;/span&gt; - mastodon    &lt;span class=&quot;token comment&quot;&gt;# switch to Mastodon user&lt;/span&gt;
$ &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; live               &lt;span class=&quot;token comment&quot;&gt;# go inside the installation folder&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rf&lt;/span&gt; vendor         &lt;span class=&quot;token comment&quot;&gt;# delete the installed packages&lt;/span&gt;
$ bundle &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;        &lt;span class=&quot;token comment&quot;&gt;# install all packages again&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, yeah, if you are experiencing the above errors as well, just reinstall your dependencies.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Translating things with a comma? Too hard for this language model.</title>
    <link href="https://jasminchen.dev/notes/2023/what-the-fuck-libretranslate/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>fa3bbff853c5fe8dc086a5afa54503bae1d74cb7</id>
    <content type="html">&lt;p&gt;Sometimes I don’t understand computers. Take, for example, the following sentence.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;sorry for the short outage, got distracted during the update :blobcat_dead:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;How hard it could be to translate it?
&lt;a href=&quot;https://queer.group/@xenia/110674393043295095&quot;&gt;Obviously very hard&lt;/a&gt;. The translation is just the
emoji.&lt;/p&gt;
&lt;p&gt;You know what’s even funnier? If you remove the comma, everything works. You could even place the
comma at a totally different location and the translation still works. Yes, I’ve tested it.&lt;/p&gt;
&lt;p&gt;But sure, “AI” is gonna save the world. /i&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;(not blaming the developers here, they’re doing their best. but it’s still fascinating tho.)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Meine erste GPN</title>
    <link href="https://jasminchen.dev/notes/2023/meine-erste-gpn/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>15f0c1c5657bd8e54d549c3a0cde3b802242fab7</id>
    <content type="html">&lt;p&gt;Letztes Wochenende hab ich meine erste &lt;em&gt;Gulaschprogrammiernacht&lt;/em&gt; (GPN) besucht. Bereits im Vorfeld
zeigte sich, dass das indirekte Motto der &lt;em&gt;Gulash pride night&lt;/em&gt; eher zutreffen könnte und für mich
war es das auch.&lt;/p&gt;
&lt;p&gt;Ein Wochenende, in dem es für mich darum ging, nette Menschen kennenzulernen. Und das war so
verdammt befreiend. Bis auf einen Zwischenfall gab es keine nervigen Typen, kein Geglotze, afaik
keinen &lt;em&gt;fefe&lt;/em&gt; und keine misogynen Kackbratzen. Oder sie haben sich einfach sehr unauffällig
verhalten.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Es war sehr schön und sehr bunt und sehr queer.&lt;/strong&gt; Und ich hab meine ersten Furries kuscheln dürfen
🥺&lt;/p&gt;
&lt;p&gt;Danke. Danke für diese Erlebnisse. Für all die netten Gespräche. Für das Kuscheln. Für den
gegenseitigen Respekt. Danke, dass es ein Event gibt, in dem ich einfach auch ich selbst sein kann.
Ganz egal, ob das gerade bedeutet, dass ich meine Ruhe brauche oder halb nackt (innerhalb des
Rahmens) über das Gelände hüpfe.&lt;/p&gt;
&lt;h2 id=&quot;kurzer-covid-einwurf-zum-schluss&quot;&gt;Kurzer COVID-Einwurf zum Schluss&lt;/h2&gt;
&lt;p&gt;Ich hätte mir aber dennoch gewünscht, dass deutlich mehr Menschen Maske tragen. Die Anzahl an
Maskenträger*innen war leider echt unterirdisch. Ich kann ja verstehen, dass das zum Essen
unpraktisch ist. Und auch in der Küche, wo es warm ist, kann das eventuell nervig sein. Aber
dennoch, Leute, bisschen Respekt und so wäre super.&lt;/p&gt;
&lt;p&gt;Oder wenigstens irgendwie Schutzkonzepte in Form von einer PCR-Test-Station oder halt, &lt;em&gt;irgendwas&lt;/em&gt;.
Hoffen wir einfach, dass meine Tests weiterhin negativ bleiben.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How I fixed my ThinkPad&#39;s display</title>
    <link href="https://jasminchen.dev/notes/2023/thinkpads-are-pretty-cool/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>703d4d3ce3d30c96732a3e7bb522fe631d945484</id>
    <content type="html">&lt;p&gt;Sometimes it’s the little things that matter. For those who don’t follow me on Mastodon, the display
on my older ThinkPad T460 broke. It didn’t even fell or something like that, it was just broken
within a minute.&lt;/p&gt;
&lt;p&gt;I mean, I could’ve still used it. But if one quarter of your screen is flickering, that’s annoying.
And I hate annoying things. A lot. So I wanted to fix it. Luckily, there’s an
&lt;a href=&quot;https://de.ifixit.com/Anleitung/Lenovo+ThinkPad+T460+LCD+Panel+Replacement/143410&quot;&gt;iFixit guide for the LCD replacement&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Can’t be that hard to follow a guide, eh?&lt;/p&gt;
&lt;h2 id=&quot;the-first-disassembly&quot;&gt;The first disassembly&lt;/h2&gt;
&lt;p&gt;(insert a &lt;em&gt;dis ass can fit so many embly&lt;/em&gt; joke here)&lt;/p&gt;
&lt;p&gt;Sorry, had to do it. When I first followed the guide, I had the hope that it was just a problem with
the contacts. That I just need to clean the contacts and everything will be working after that.
&lt;strong&gt;Turns out, I was wrong.&lt;/strong&gt; I’ve disassambled the whole part, only to find out that it didn’t
improve anything. It didn’t got worse, so at least I haven’t broken something. One of the fears that
you have when you don’t this every day 😅&lt;/p&gt;
&lt;p&gt;Anyway, I asked the Matrix room of the local chaos community. If someone uses ThinkPads, then
definitely hackers. And catgirls, puppygirls, foxgirls, you name it. I got forwarded to the
&lt;a href=&quot;https://www.xelent-store.de/400nit-Display-fuer-Lenovo-Thinkpad-T470-IPS-Full-HD-1920x1080-Neuware&quot;&gt;Xelent store&lt;/a&gt;.
The display that I’ve linked is actually better than the default. If I were to buy a new display, at
least I can get an upgrade as well, eh?&lt;/p&gt;
&lt;p&gt;That’s what I did. The shop is based in Germany, so a plus for customer right protections. If it
wouldn’t have fit properly, I could send it back. But I’ve checked the number of pins and even
peeked under the base cover of my partners T470 to be &lt;em&gt;extra&lt;/em&gt; sure.&lt;/p&gt;
&lt;p&gt;It took quite a while, around three weeks. But one day it arrived, hooray!&lt;/p&gt;
&lt;h2 id=&quot;the-actual-replacement&quot;&gt;The actual replacement&lt;/h2&gt;
&lt;p&gt;Actually, having a new display in front of me was quite scary. It’s a lot of money that could be
destroyed with a simple mistake. I was really really careful with the disassembly and all the
screws. I really wanted this to succeed.&lt;/p&gt;
&lt;p&gt;So I followed the above guide again, step by step. With dramatic music in the background and no
lights whatsoever, I tried to fix it. Of course, I’m exaggerating. Neither the eye of Sauron nor
some dark music was following me. I am used to play some 80s rock in the background, and that’s what
I did this time as well. (&lt;em&gt;At least I think I did, I actually don’t know it for sure.&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;Around half a hour later, I &lt;strong&gt;got it working&lt;/strong&gt;! I really did that! I really fixed the display! One
of the core components, one of the harder parts! &lt;strong&gt;I did it!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’m really really proud of myself. And now I’ve got an even better display than it was before.
Checked it with the built-in maintainability tool, all the pixels are working.&lt;/p&gt;
&lt;p&gt;And with that, I am now ready for the &lt;a href=&quot;https://entropia.de/GPN21&quot;&gt;GPN&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>TIL: What&#39;s a search domain?</title>
    <link href="https://jasminchen.dev/notes/2023/search_domains/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>a011cda9fecdbcce82036ff6ffb49c6459e9ed5e</id>
    <content type="html">&lt;p&gt;I never thought about the &lt;em&gt;search domain&lt;/em&gt; setting in my network settings. It was set to &lt;code&gt;fritz.box&lt;/code&gt;
(cause I use one of their routers) and I never knew why. It was just… there.&lt;/p&gt;
&lt;p&gt;Because of a &lt;a href=&quot;https://catcatnya.com/@benaryorg/110373926009472104&quot;&gt;toot by Katze&lt;/a&gt;, I got a bit
hooked what that thing called actually is.&lt;/p&gt;
&lt;h2 id=&quot;what-s-the-search-domain&quot;&gt;What’s the search domain?&lt;/h2&gt;
&lt;p&gt;Basically, if you have a search domain set, like my router did it, you can resolve arbitrary, uh,
subdomains. It’s useful for company networks and especially internal DNS systems.&lt;/p&gt;
&lt;p&gt;For example: If you set the search domain to &lt;code&gt;acme.org&lt;/code&gt;, people can resolve &lt;code&gt;meow.acme.org&lt;/code&gt; by
simply entering &lt;code&gt;meow&lt;/code&gt; in their browser. The domain would be resolved like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ooooh, you don’t have a &lt;em&gt;top level domain&lt;/em&gt; on this request. Imma ask &lt;code&gt;acme.org&lt;/code&gt; to resolve this.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acme.org&lt;/code&gt; responded with &lt;code&gt;10.0.13.12&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I can send my request to &lt;code&gt;10.0.13.12&lt;/code&gt;, yaay! :3&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;can-i-use-that-for-my-own-network&quot;&gt;Can I use that for my own network?&lt;/h2&gt;
&lt;p&gt;Theoretically yes. Practically, it’s complicated. The reason for that is
&lt;a href=&quot;https://en.wikipedia.org/wiki/DNS_rebinding&quot;&gt;DNS rebinding&lt;/a&gt;, a method of DNS spoofing. The
technical details are explained in the linked Wikipedia article.&lt;/p&gt;
&lt;p&gt;However, as a safety precaution, certain routers, like my beloved (&lt;em&gt;ahem&lt;/em&gt;) Fritz!Box do have a
filter for such DNS requests. Even if I’d set the A record for &lt;code&gt;home.jasminchen.dev&lt;/code&gt; to
&lt;code&gt;192.168.178.100&lt;/code&gt;, the router would block such DNS requests.&lt;/p&gt;
&lt;p&gt;With &lt;em&gt;DNS over HTTPS&lt;/em&gt; (DoH) or &lt;em&gt;DNS over TLS&lt;/em&gt; (DoT), I could easily circumvent such filters. But
that’s something to explore on another day.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Determine the Go architecture in a justfile</title>
    <link href="https://jasminchen.dev/notes/2023/determine_the_go_architecture_in_a_justfile/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>cd1e902ee4429356ead15b242415d03149e052d6</id>
    <content type="html">&lt;p&gt;These days, &lt;a href=&quot;https://github.com/casey/just&quot;&gt;just&lt;/a&gt; is my favorite tool to invoke build scripts, run
deploy commands etc. It’s just a binary, the &lt;code&gt;justfile&lt;/code&gt; syntax is clean, there’s nothing to hate
about it.&lt;/p&gt;
&lt;p&gt;Unfortunately, when using a lot of Go-based programs, there’s a slight mismatch between the
architecture reported by the &lt;code&gt;arch()&lt;/code&gt; function and the architecture used by the Go compiler. As of
today, there are about
&lt;a href=&quot;https://github.com/golang/go/blob/3e9876cd3a5a83be9bb0f5cbc600aadf9b599558/src/go/build/syslist.go#L56&quot;&gt;20 reserved architectures&lt;/a&gt;
reserved by the Go team. Not all of them are used (anymore).&lt;/p&gt;
&lt;p&gt;But if I want to download a Go-based tool, like &lt;a href=&quot;https://github.com/amacneil/dbmate&quot;&gt;dbmate&lt;/a&gt; for
running the database migrations, I either need to hardcode the architecture to &lt;code&gt;amd64&lt;/code&gt; or map from
&lt;code&gt;x86_64&lt;/code&gt; to &lt;code&gt;amd64&lt;/code&gt;. I would’ve used the hardcoded &lt;code&gt;amd64&lt;/code&gt; value in the past, cause ARM
architectures were not as common back then. But, with the increased popularity of ARM for servers
and clients, this is not longer suitable.&lt;/p&gt;
&lt;p&gt;Therefore, I wrote this handy, &lt;code&gt;just&lt;/code&gt;-native ugly &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;else&lt;/code&gt;-block, which covers the most common
architectures:&lt;/p&gt;
&lt;pre class=&quot;language-justfile&quot;&gt;&lt;code class=&quot;language-justfile&quot;&gt;# Maps arch() to one of the known values inside the syslist.go
# https://github.com/golang/go/blob/3e9876cd3a5a83be9bb0f5cbc600aadf9b599558/src/go/build/syslist.go#L56
go_arch := if arch() ==&quot;aarch64&quot; {
    &quot;arm64&quot;
} else if arch() == &quot;arm&quot; {
    &quot;arm&quot;
} else if arch() == &quot;wasm32&quot; {
    &quot;wasm&quot;
} else if arch() == &quot;x86&quot; {
    &quot;386&quot;
} else if arch() == &quot;x86_64&quot; {
    &quot;amd64&quot;
} else { error(&quot;unknown architecture {{ os() }}&quot;) }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It can be even extended to cover almost all supported architectures:&lt;/p&gt;
&lt;pre class=&quot;language-justfile&quot;&gt;&lt;code class=&quot;language-justfile&quot;&gt;# Maps arch() to one of the known values inside the syslist.go
# https://github.com/golang/go/blob/3e9876cd3a5a83be9bb0f5cbc600aadf9b599558/src/go/build/syslist.go#L56
go_arch_complete := if arch() ==&quot;aarch64&quot; {
    &quot;arm64&quot;
} else if arch() == &quot;arm&quot; {
    &quot;arm&quot;
} else if arch() == &quot;mips&quot; {
    &quot;mips64&quot;
} else if arch() == &quot;powerpc&quot; {
    &quot;ppc&quot;
} else if arch() == &quot;powerpc64&quot; {
    &quot;ppc64&quot;
} else if arch() == &quot;s390x&quot; {
    &quot;s390x&quot;
} else if arch() == &quot;sparc&quot; {
    &quot;sparc64&quot;
} else if arch() == &quot;wasm32&quot; {
    &quot;wasm&quot;
} else if arch() == &quot;x86&quot; {
    &quot;386&quot;
} else if arch() == &quot;x86_64&quot; {
    &quot;amd64&quot;
} else { error(&quot;unknown architecture {{ os() }}&quot;) }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I can use the variable to determine the correct download URL for &lt;em&gt;dbmate&lt;/em&gt;, just like this:&lt;/p&gt;
&lt;pre class=&quot;language-justfile&quot;&gt;&lt;code class=&quot;language-justfile&quot;&gt;install-tools:
    curl -fsSL -o ./bin/dbmate https://github.com/amacneil/dbmate/releases/download/{{ TOOLS_DBMATE_VERSION }}/dbmate-{{ os() }}-{{ go_arch }}
    chmod +x ./bin/dbmate&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Enabling accent character selection on KDE Plasma</title>
    <link href="https://jasminchen.dev/notes/2023/accented-characters-in-kde/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>d6c3c1f736a236b99652353f909bac658e419492</id>
    <content type="html">&lt;p&gt;After re-installing my Arch setup from scratch and installing my favorite desktop environment, KDE
Plasma, again, I realized that I was missing one specific feature: holding down the “regular” keys
to get a selection menu for accented/alternative characters.&lt;/p&gt;
&lt;p&gt;An example? One of my metamours has the &lt;em&gt;à&lt;/em&gt; inside their name. Unfortunately, I’m lazy and out of
this laziness I first chose the regular &lt;em&gt;à&lt;/em&gt;. Although I think that it’s not a big deal, I still find
it disrespectful.&lt;/p&gt;
&lt;p&gt;Luckily, I found
&lt;a href=&quot;https://pointieststick.com/2020/12/04/this-week-in-kde-big-new-accented-alternative-character-input-feature/&quot;&gt;Nate Graham’s blog post&lt;/a&gt;
about said feature really quick. I’ve opened vim, edited &lt;code&gt;/etc/environment&lt;/code&gt; and added the
&lt;code&gt;QT_IM_MODULE=plasmaim&lt;/code&gt; line.&lt;/p&gt;
&lt;p&gt;Voila, now it’s enabled and I can write the name of my metamour as intended! 😄&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Note (2023-08-14):&lt;/strong&gt; It looks like the above environment variable
&lt;a href=&quot;https://bugs.kde.org/show_bug.cgi?id=411729&quot;&gt;breaks with the so-called “dead keys”&lt;/a&gt;, e.g.
&lt;code&gt;^&lt;/code&gt;. Therefore, I just set the &lt;code&gt;QT_IM_MODULE&lt;/code&gt; to the empty string, in order to have my dead keys on
Wayland again.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Removing orphans from the Mastodon cache</title>
    <link href="https://jasminchen.dev/notes/2023/removing-orpahns-from-the-mastodon-cache/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>cabb0f6b1234b2756a355fb3b5e2e70390f0c82d</id>
    <content type="html">&lt;p&gt;As part of the ongoing migration of the &lt;a href=&quot;https://queer.group&quot;&gt;queer.group&lt;/a&gt; media files from local
storage to S3, I’ve realised that I should do a cleanup of the orphaned files &lt;em&gt;before&lt;/em&gt; copying them
to the S3 bucket. Fortunately, Mastodon offers an easy solution with its `tootctl’ command.&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;tootctl media remove-orphans&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And very soon, files got deleted.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# removed for brevity
Found and removed orphan: cache/accounts/headers/109/384/607/004/860/065/original/039a300fdeba0dc0.jpg
Found and removed orphan: cache/accounts/headers/109/427/244/149/514/100/original/b464439464ab98ff.jpeg
Found and removed orphan: cache/accounts/headers/109/440/021/041/888/306/original/04dda84036e651b6.png
Found and removed orphan: cache/accounts/headers/109/440/021/041/888/306/original/2777348e3afd634d.png
Found and removed orphan: cache/accounts/headers/109/440/021/041/888/306/original/2aa59f9af035ba23.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During the removal process, it crashed after about 200,000 scanned files. This was probably due to a
temporary database error. But it has a &lt;code&gt;--start-after&lt;/code&gt; parameter with a very unhelpful explanation,
I would say.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Paperclip attachment key where the loop will start. Use this option if the command was
interrupted before.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What the heck is a “Paperclip attachment key”? Where do I find it? Turns out, it’s &lt;strong&gt;the filename&lt;/strong&gt;.
I marked it in the following quote.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Found and removed orphan:
&lt;strong&gt;cache/accounts/headers/109/440/021/041/888/306/original/2aa59f9af035ba23.png&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To provide an example command on how to use it, here’s how I did it:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;RAILS_ENV&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;production bin/tootctl media remove-orphans --start-after&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;cache/accounts/headers/109/440/021/041/888/306/original/2aa59f9af035ba23.png&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Proxying S3 with NGINX to Backblaze B2</title>
    <link href="https://jasminchen.dev/notes/2023/proxying-s3-with-nginx-to-backblaze-b2/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>0163d40419730672ba3438e2dfed8953b36e9cf8</id>
    <content type="html">&lt;p&gt;Sometimes a simple forward slash (&lt;code&gt;/&lt;/code&gt;) can drive a lot of frustration. For the migration of
&lt;a href=&quot;https://queer.group&quot;&gt;our Mastodon instance&lt;/a&gt; from local storage to a S3-compatible one, I’ve wanted
to do it without any bigger service disruptions. To do that, I’ve followed
&lt;a href=&quot;https://leah.is/posts/scaling-the-mastodon/&quot;&gt;Leah’s post about Mastodon scaling&lt;/a&gt;. She states:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also build everything to allow migration in the background with new media directly
uploaded and served to/by S3 and old media, that isn’t migrated yet, served as fallback by the old
local storage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes! This is exactly what I want! In the full config example that she’s linked to, this line caused
the frustration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_pass https://s3.example.com/mastodon/$uri;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My assumption was that &lt;code&gt;mastodon&lt;/code&gt; is the S3 bucket name. Fortunately, Backblaze provides hostnames
where the bucket name is already included, like &lt;code&gt;https://bucket.s3.example.com&lt;/code&gt;. Therefore I’ve
updated the line to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_pass https://my-backblaze-bucket.s3.eu-central-003.backblazeb2.com/$uri;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks similar to the config above, right? Except: it doesn’t work. The reason for that is: &lt;code&gt;$uri&lt;/code&gt;
starts with a forward slash (&lt;code&gt;/&lt;/code&gt;) and therefore the proxied URLs look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://my-backblaze-bucket.s3.eu-central-003.backblazeb2.com//something.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You see that double slash? Normally this wouldn’t be a problem, as hosting providers can easily get
rid of them. &lt;strong&gt;Backblaze doesn’t&lt;/strong&gt;. Therefore, in order to make the &lt;code&gt;proxy_pass&lt;/code&gt; setting work as
intended, the line needs to look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_pass https://my-backblaze-bucket.s3.eu-central-003.backblazeb2.com$uri;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s all. That single character.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Side note:&lt;/strong&gt; According to a
&lt;a href=&quot;https://stackoverflow.com/questions/48708361/nginx-request-uri-vs-uri#comment91076347_48709976&quot;&gt;comment made on StackOverflow&lt;/a&gt;
you shouldn’t use &lt;code&gt;$uri&lt;/code&gt;, but &lt;code&gt;$request_uri&lt;/code&gt;, because &lt;code&gt;$uri&lt;/code&gt; &lt;em&gt;might&lt;/em&gt; open the instance up for HTTP
header injection vulnerabilities. All in all, the config line should look like this now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_pass https://my-backblaze-bucket.s3.eu-central-003.backblazeb2.com$request_uri;
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Why I think that Conventional Commits are a bad idea</title>
    <link href="https://jasminchen.dev/notes/2022/why-conventional-commits-are-a-bad-idea/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>c4cb6af04afde4a6a078a90c484a14ef9cfd905a</id>
    <content type="html">&lt;p&gt;When writing commit messages, we have a lot of flexibility. We can write the largest commit message
that the world has seen until then or we can do a &lt;code&gt;git commit -m &amp;quot;wip&amp;quot;&lt;/code&gt; and that’s it.&lt;/p&gt;
&lt;p&gt;Writing commit messages takes a lot of practice, no doubt. There are guides, patterns and all that
stuff around the internet. One example are
&lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot;&gt;Conventional Commits&lt;/a&gt;. Conventions and standards
are good, at least most of the times. However, I’m not sure if this is the case with conventional
commits.&lt;/p&gt;
&lt;p&gt;If you are unfamiliar with it, here’s the quote from their page:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Conventional Commits specification is a lightweight convention on top of commit messages. It
provides an easy set of rules for creating an explicit commit history; which makes it easier to
write automated tools on top of. This convention dovetails with &amp;gt; SemVer, by describing the
features, fixes, and breaking changes made in commit messages.&lt;/p&gt;
&lt;p&gt;The commit message should be structured as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;type&amp;gt;[optional scope]: &amp;lt;description&amp;gt;

[optional body]

[optional footer(s)]
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sounds easy, right? Now we have a type, a scope and that’s it. And the types are &lt;code&gt;fix&lt;/code&gt;, &lt;code&gt;feat&lt;/code&gt;,
&lt;code&gt;refactor&lt;/code&gt; and some else. That’s not a lot of new concepts, nice! I’ve adopted Conventional Commits
around 2021 and have been using them since.&lt;/p&gt;
&lt;h2 id=&quot;simplicity-does-not-exist&quot;&gt;Simplicity does not exist.&lt;/h2&gt;
&lt;p&gt;Although it sounds easy to prefix your commit messages with a type, that part of categorization is
actually hard. At least for me.&lt;/p&gt;
&lt;h3 id=&quot;example-updating-a-major-flaw-in-the-docs&quot;&gt;Example: Updating a major flaw in the docs&lt;/h3&gt;
&lt;p&gt;There’s a major problem with the docs, they’re out of date and they written behaviour leads to
unexpected results. Let’s assume that the commit actually solves this problem. Because it’s going to
fix the instructions for new users, it’s probably a &lt;code&gt;fix&lt;/code&gt;. Maybe even a &lt;code&gt;fix(docs)&lt;/code&gt;. Or does it
belong to the &lt;code&gt;docs&lt;/code&gt; scope? It could even be a &lt;code&gt;chore&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It depends on the personal perception of the issue severity. I wouldn’t judge anyone for picking the
wrong scope.&lt;/p&gt;
&lt;h2 id=&quot;guidelines-jasmin-guidelines&quot;&gt;Guidelines, Jasmin, guidelines!&lt;/h2&gt;
&lt;p&gt;Yes, of course, there should be a guideline for the team. But even then, writing good commit
messages is hard. &lt;code&gt;git&lt;/code&gt; itself is a monster. &lt;strong&gt;No one should deal with &lt;code&gt;git&lt;/code&gt;.&lt;/strong&gt; I understand it very
well (at least I think so). I’m working years with this thing. I’m no longer “deleting and
re-cloning” repositories, just because I broke something. Nevertheless, it’s still a shitty
software. It works, but it’s definitely not user-friendly.&lt;/p&gt;
&lt;h2 id=&quot;but-what-about-the-automation&quot;&gt;But what about the automation?&lt;/h2&gt;
&lt;p&gt;Actually I don’t think that auto-generated changelogs are useful. If your changelog is just a
categorization of your commits, it does not provide any value to me. I don’t care about the hundreds
and thousands commits by &lt;code&gt;@dependabot&lt;/code&gt;. Either admit that you don’t want to write a changelog or
write one. Yes, it’s work. Yes, it’s annoying. But it needs to be done.&lt;/p&gt;
&lt;h2 id=&quot;and-now&quot;&gt;And now?&lt;/h2&gt;
&lt;p&gt;Actually, I don’t have a proper solution for writing commits. But I know that I won’t be writing
&lt;em&gt;Conventional Commits&lt;/em&gt; much longer than needed. Instead, I use the good old style: regular titles.
Instead of &lt;code&gt;feat: add new menu button&lt;/code&gt; I’m going to write &lt;code&gt;Add new menu button&lt;/code&gt;. And I’ll put more
effort into my changelogs.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>hello_world.md</title>
    <link href="https://jasminchen.dev/notes/2022/hello_world/" />
    <updated>2026-06-01T21:17:25Z</updated>
    <id>6d54712d1caad8d2c5e7e62be9caa82ad42af388</id>
    <content type="html">&lt;p&gt;So, let’s try it out again. Maybe I’ll actually write a blog &lt;em&gt;this time&lt;/em&gt;. Would be nice. But I
cannot promise anything. Maybe this is also the goodbye post, who knows.&lt;/p&gt;
&lt;p&gt;but for now: &lt;strong&gt;Hi.&lt;/strong&gt; 👋&lt;/p&gt;
</content>
  </entry>
</feed>
