<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>show tech · cenko.tech</title><description>Notes from troubleshooting, homelab builds and web development, written down so the fix outlives the ticket.</description><link>https://cenko.tech/</link><language>en</language><item><title>Making apt repository setup fail where the problem is</title><link>https://cenko.tech/blog/bootstrap-linux-pc-fail-safe-apt-keys/</link><guid isPermaLink="true">https://cenko.tech/blog/bootstrap-linux-pc-fail-safe-apt-keys/</guid><description>A Linux bootstrap script kept going when a repo signing key failed to download, so the error showed up later as an unrelated apt failure. Here is the fix.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/alexziskind1/bootstrap_linux_pc&quot;&gt;bootstrap_linux_pc&lt;/a&gt; sets up a fresh Fedora or Ubuntu machine with one command. On Ubuntu it adds the Google Chrome and VS Code apt repositories before installing packages from them.&lt;/p&gt;
&lt;h2 id=&quot;the-symptom&quot;&gt;The symptom&lt;/h2&gt;
&lt;p&gt;If something goes wrong while adding a repository, you do not find out there. You find out a few steps later, when &lt;code&gt;apt-get update&lt;/code&gt; or a package install fails with an error that says nothing about signing keys. I wrote this up as &lt;a href=&quot;https://github.com/alexziskind1/bootstrap_linux_pc/issues/3&quot;&gt;issue #3&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-cause&quot;&gt;The cause&lt;/h2&gt;
&lt;p&gt;Each repository was added like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; https://dl.google.com/linux/linux_signing_key.pub&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  |&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; sudo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; gpg&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; --dearmor&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; /usr/share/keyrings/google-chrome.gpg&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  |&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; sudo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; tee&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; /etc/apt/sources.list.d/google-chrome.list&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;/dev/null&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing checks whether the key download or the &lt;code&gt;gpg --dearmor&lt;/code&gt; step worked. If the download fails, the script carries on and still writes the &lt;code&gt;sources.list.d&lt;/code&gt; entry. Apt is now pointed at a repository it has no valid key for, and the failure surfaces later, somewhere else.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/alexziskind1/bootstrap_linux_pc/pull/4&quot;&gt;pull request&lt;/a&gt; splits the job into two functions.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;add_apt_repo_key()&lt;/code&gt; downloads the key to a temporary file, checks the download succeeded and is not empty, then dearmors it. If dearmoring fails, it removes any partial keyring file:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; curl&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;$url&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;$tmp_key&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  err&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;Failed to download signing key from $url&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  rm&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;$tmp_key&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [[ &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;$tmp_key&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; ]]; &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  err&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;Downloaded signing key from $url is empty.&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  rm&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;$tmp_key&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;add_apt_repo()&lt;/code&gt; only writes the repository file if the key step succeeded. If it did not, it names the repository that failed and skips it, instead of leaving a repo file with no matching key:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; add_apt_repo_key&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;$key_url&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;$keyring_path&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;  warn&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;Skipping $name repository setup — $name install will be skipped.&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both repositories now go through &lt;code&gt;add_apt_repo&lt;/code&gt;. The already-configured path and the normal success path behave exactly as before.&lt;/p&gt;
&lt;h2 id=&quot;how-i-tested-it&quot;&gt;How I tested it&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash -n distro/ubuntu.sh&lt;/code&gt; for syntax.&lt;/li&gt;
&lt;li&gt;An Ubuntu 24.04 VM with the key URL blocked through an &lt;code&gt;/etc/hosts&lt;/code&gt; entry, to confirm the script now warns and skips cleanly.&lt;/li&gt;
&lt;li&gt;A normal run on the same VM, to confirm both repositories are still configured.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;callout callout--note&quot; aria-label=&quot;note&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;note:&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;The pull request is still open, waiting for the maintainer’s review.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;The general rule: when a step is a prerequisite for later steps, check it where it happens. An error message that names the real cause saves the next person a long search.&lt;/p&gt;</content:encoded><category>linux</category><category>bash</category><category>ubuntu</category><category>open-source</category></item><item><title>When the AI says the teams are balanced and they are not</title><link>https://cenko.tech/blog/footbolski-ai-team-split/</link><guid isPermaLink="true">https://cenko.tech/blog/footbolski-ai-team-split/</guid><description>Five pull requests to footbolski, a match-organising app for our football group, including tracing why its AI team split contradicted its own explanation.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/rezart95/footbolski&quot;&gt;footbolski&lt;/a&gt; is the app our football group uses to organise matches: people register for a game, an admin keeps player ratings, and an AI splits the confirmed players into two teams. Between August and September I sent it five pull requests. The most interesting one taught me something about where an LLM belongs in a system.&lt;/p&gt;
&lt;h2 id=&quot;the-bug-reasoning-that-does-not-match-the-result&quot;&gt;The bug: reasoning that does not match the result&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rezart95/footbolski/issues/30&quot;&gt;Issue #30&lt;/a&gt;: the AI’s explanation said the two highest-rated players were on opposite teams, but the generated teams had both of them on Team A.&lt;/p&gt;
&lt;p&gt;I traced the pipeline from the team service through the name-matching code to the prompt. The name matching was correct: it case-folds both sides consistently, so this was not a lookup bug. The real problem was that the reasoning text was saved and shown exactly as the model wrote it, and nothing checked its claims against the team lists the model returned in the same response. The prompt told the model not to put the strongest players together, but nothing enforced that.&lt;/p&gt;
&lt;p&gt;My &lt;a href=&quot;https://github.com/rezart95/footbolski/pull/31&quot;&gt;pull request&lt;/a&gt; added a deterministic backstop. After the AI’s split is mapped to real players, it checks whether the two highest-scoring players landed on the same side. If they did, it swaps the weaker of the two with the strongest player on the other team, keeping team sizes equal, and adds a note to the displayed reasoning so the admin can see a correction was made.&lt;/p&gt;
&lt;h2 id=&quot;what-the-maintainer-found-next&quot;&gt;What the maintainer found next&lt;/h2&gt;
&lt;p&gt;The maintainer tested the pipeline against a real upcoming match, five live runs. In four of them, the model put the two strongest players on the same team and claimed the opposite. Asked to rank the squad, it returned the correct order, so it knew who was best. And an exhaustive search over every possible split found a far more even one in milliseconds than anything the model produced.&lt;/p&gt;
&lt;p&gt;Their conclusion: the backstop works, but it patches one rule and ships the contradiction to users with an apology attached. They pushed a redesign onto the same branch. The model no longer picks teams. It reads the scouting notes and returns structured judgments about each player. Code validates those judgments, a solver finds the actual best split, and only then does the model write an explanation, from the finished split. It cannot describe teams that were not produced.&lt;/p&gt;
&lt;aside class=&quot;callout callout--tip&quot; aria-label=&quot;tip&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;tip:&lt;span&gt; the takeaway&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;Use the language model for what it is good at: reading free text and explaining. Give combinatorial search to code. And never show a model’s claims about its output without checking them against the output.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;h2 id=&quot;the-other-four-pull-requests&quot;&gt;The other four pull requests&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rezart95/footbolski/pull/23&quot;&gt;Join WhatsApp group step&lt;/a&gt;.&lt;/strong&gt; New players register in the app but are not necessarily in the WhatsApp group where matches are organised. I added a step to first-time registration: open the group link, then confirm. The confirm checkbox stays disabled until the link has been opened. It is a nudge, not verification, because WhatsApp gives the app no signal when someone joins.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rezart95/footbolski/pull/25&quot;&gt;Aerial on player cards, a new Passing rating&lt;/a&gt;.&lt;/strong&gt; The admin had to open every player’s detail view to see the Aerial rating, so I put it on the card. I also added a Passing rating for the team splitter to weigh and, at the admin’s request, removed eight free-text style tags the AI could not meaningfully compare. In the same area I found an import of a function that did not exist, which would fail at runtime and likely meant the AI split path was already broken, and fixed that too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rezart95/footbolski/pull/27&quot;&gt;Backfill for older player cards&lt;/a&gt;.&lt;/strong&gt; The Passing column had no value for players created before it existed, so their cards showed “-/10”. A one-time database migration sets those to the default of 5. Its downgrade is deliberately a no-op, because undoing it could erase real ratings an admin had since set to 5.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rezart95/footbolski/pull/29&quot;&gt;Existing members blocked by the WhatsApp step&lt;/a&gt;.&lt;/strong&gt; On a new phone or in a private window, existing members were asked to join a group they were already in. The root cause: “new user” was decided purely from what this browser had stored. The fix also checks the typed name against the real player list, using a full-name match so a stranger who shares a first name with a member still sees the step.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>ai</category><category>python</category><category>typescript</category><category>debugging</category><category>open-source</category></item><item><title>Only works if your username is &quot;user&quot;: fixing hardcoded home paths</title><link>https://cenko.tech/blog/opencode-voice-hardcoded-home-paths/</link><guid isPermaLink="true">https://cenko.tech/blog/opencode-voice-hardcoded-home-paths/</guid><description>A voice-control tool hardcoded /home/user in its scripts and systemd unit, so it broke for every other account. $HOME and systemd&apos;s %h fixed it.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/toor11/opencode-voice&quot;&gt;opencode-voice&lt;/a&gt; adds push-to-talk voice control for the OpenCode CLI on Hyprland/Wayland: hold a key, speak, release. It is a set of shell scripts, a Python transcription daemon and a systemd user service.&lt;/p&gt;
&lt;h2 id=&quot;the-symptom&quot;&gt;The symptom&lt;/h2&gt;
&lt;p&gt;Reading the code, I noticed several scripts and the systemd unit pointed at &lt;code&gt;/home/user/...&lt;/code&gt;. That only works if your login name is literally &lt;code&gt;user&lt;/code&gt;. On any other account:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the scripts put a directory that does not exist on &lt;code&gt;PATH&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPENCODE_BIN&lt;/code&gt; points at a binary that is not there,&lt;/li&gt;
&lt;li&gt;the systemd service cannot start, because its &lt;code&gt;ExecStart&lt;/code&gt; path does not exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I reported it as &lt;a href=&quot;https://github.com/toor11/opencode-voice/issues/1&quot;&gt;issue #1&lt;/a&gt;. I found it by reading the code, not by running it on a second account, and the issue says so.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;Shell scripts get the home directory from &lt;code&gt;$HOME&lt;/code&gt;, and systemd user units get it from the &lt;code&gt;%h&lt;/code&gt; specifier.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# before&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; PATH&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;/home/user/.opencode/bin:/home/user/.local/bin:/usr/bin:/bin&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;OPENCODE_BIN&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;/home/user/.opencode/bin/opencode&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# after&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; PATH&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;$HOME/.opencode/bin:$HOME/.local/bin:/usr/bin:/bin&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;OPENCODE_BIN&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;${OC_VOICE_OPENCODE&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:-&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;$HOME&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;.opencode&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;bin&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;opencode}&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# opencode-voice.service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# %h expands to the user&amp;#39;s home; edit if the repo lives elsewhere.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;ExecStart=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;%h/Projects/opencode-voice/voice-daemon.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OC_VOICE_OPENCODE&lt;/code&gt; is a new, optional override for people who installed OpenCode somewhere else. The default location is unchanged, and the README mentions the new variable.&lt;/p&gt;
&lt;h2 id=&quot;testing-honestly&quot;&gt;Testing, honestly&lt;/h2&gt;
&lt;p&gt;I was on Windows, so I could not run the project’s unit tests (they execute the shell scripts directly). What I did check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash -n&lt;/code&gt; on every script and &lt;code&gt;python -m py_compile&lt;/code&gt; on the Python files.&lt;/li&gt;
&lt;li&gt;A dry run as a pretend user: &lt;code&gt;HOME=/home/alice OC_VOICE_DRYRUN=1 bash voice-cmd.sh &amp;quot;open opencode and start working&amp;quot;&lt;/code&gt; resolves the binary to &lt;code&gt;/home/alice/.opencode/bin/opencode&lt;/code&gt;, and &lt;code&gt;OC_VOICE_OPENCODE=/opt/oc&lt;/code&gt; overrides it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -rn /home/user&lt;/code&gt; over the repository finds nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/toor11/opencode-voice/pull/2&quot;&gt;pull request&lt;/a&gt; listed what was and was not tested, and the maintainer merged it the same day.&lt;/p&gt;
&lt;aside class=&quot;callout callout--tip&quot; aria-label=&quot;tip&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;tip:&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;In a systemd user unit, &lt;code&gt;%h&lt;/code&gt; is the home directory of the user running the service. It is the unit-file equivalent of &lt;code&gt;$HOME&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;</content:encoded><category>linux</category><category>bash</category><category>systemd</category><category>open-source</category></item><item><title>Redesigning a unit converter with no dependencies</title><link>https://cenko.tech/blog/unit-converter-redesign/</link><guid isPermaLink="true">https://cenko.tech/blog/unit-converter-redesign/</guid><description>A Node.js unit converter rebuilt with live conversion, shareable links and four new categories. It still has no npm dependencies and works without JavaScript.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/0xgitpushpray/unit-converter&quot;&gt;unit-converter&lt;/a&gt; started as a small Node.js server that converts length, weight and temperature. This &lt;a href=&quot;https://github.com/0xgitpushpray/unit-converter/pull/1&quot;&gt;pull request&lt;/a&gt; gave it a new interface and more to convert, with one rule: no npm dependencies.&lt;/p&gt;
&lt;h2 id=&quot;what-changed&quot;&gt;What changed&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;A carpenter’s-rule look.&lt;/strong&gt; An ink-coloured shell around a boxwood-yellow working surface, with a tick-mark scale that shows where your answer sits. It is responsive down to phone width.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live conversion.&lt;/strong&gt; With JavaScript on, the answer updates as you type. You can swap units, copy the result, and click any unit in the all-units table to make it the target. The scale switches between linear and logarithmic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shareable links.&lt;/strong&gt; Every conversion has a URL:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/length?value=12&amp;amp;from=inch&amp;amp;to=centimeter&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Four new categories:&lt;/strong&gt; area, volume (US customary units), speed and time, joining length, weight and temperature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It still works without JavaScript.&lt;/strong&gt; The form posts back to the same page and the server renders the result and the table. I checked this with JavaScript turned off, as well as at desktop and phone widths.&lt;/p&gt;
&lt;h2 id=&quot;keeping-it-dependency-free&quot;&gt;Keeping it dependency-free&lt;/h2&gt;
&lt;p&gt;The whole app is &lt;code&gt;server.js&lt;/code&gt; (an HTTP server using Node’s built-in modules), &lt;code&gt;converters.js&lt;/code&gt; and a few static files (the stylesheet, the browser script and the icon). &lt;code&gt;converters.js&lt;/code&gt; holds the unit definitions and conversion logic, and loads both in Node and in the browser. The server and the live page use the same code, so they cannot disagree about an answer.&lt;/p&gt;
&lt;h2 id=&quot;tests&quot;&gt;Tests&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;npm test&lt;/code&gt; runs plain &lt;code&gt;assert&lt;/code&gt; checks. The new categories got their own cases, plus one loop that guards every unit at once:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;// every unit has a display symbol, and every category converts to itself unchanged&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; cat&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;of&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; Object&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.entries&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(categories)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; u&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; Object&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.keys&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.units)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.ok&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.symbols[u]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; `&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;u&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; has a symbol`&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;    eq&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(key&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 42&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; u&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; u&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 42&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That loop catches a unit added without a display symbol, and any unit whose definition is broken badly enough that converting it to itself changes the value. The fixed cases above it check the actual factors.&lt;/p&gt;
&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;
&lt;p&gt;It needs Node.js 18 or newer:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; start&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then open &lt;a href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt;.&lt;/p&gt;
&lt;aside class=&quot;callout callout--note&quot; aria-label=&quot;note&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;note:&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;The fonts load from Google Fonts. Without a connection the page falls back to system fonts.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;</content:encoded><category>javascript</category><category>nodejs</category><category>web-development</category></item><item><title>Making a Webex bot say which space it means</title><link>https://cenko.tech/blog/webex-eurl-name-the-space/</link><guid isPermaLink="true">https://cenko.tech/blog/webex-eurl-name-the-space/</guid><description>The eurl bot&apos;s join message said &quot;this space&quot;, which means nothing once the text is pasted somewhere else. A four-line change makes it name the space.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/webex/eurl&quot;&gt;eurl&lt;/a&gt; is an open-source Webex bot that gives a Webex space a short public join link. Type &lt;code&gt;eurl url&lt;/code&gt; in a space and the bot replies with the link and who is allowed to use it.&lt;/p&gt;
&lt;h2 id=&quot;the-symptom&quot;&gt;The symptom&lt;/h2&gt;
&lt;p&gt;The reply always looked like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Only users in the domain(s) &lt;strong&gt;example.com&lt;/strong&gt; can join this space using &lt;a href=&quot;https://eurl.io/#1A2B3C-AB&quot;&gt;https://eurl.io/#1A2B3C-AB&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Inside the space, that is fine. But people copy that sentence out of the space and paste it into other chats, wikis and emails, and once it leaves the room, “this space” points at nothing. &lt;a href=&quot;https://github.com/webex/eurl/issues/12&quot;&gt;Issue #12&lt;/a&gt; reported exactly that back in 2023.&lt;/p&gt;
&lt;h2 id=&quot;the-cause&quot;&gt;The cause&lt;/h2&gt;
&lt;p&gt;The message text was a fixed string. The bot already knew the space’s name: &lt;code&gt;sendJoinDetails()&lt;/code&gt; receives the whole &lt;code&gt;publicspace&lt;/code&gt; record, and its &lt;code&gt;title&lt;/code&gt; field is stored and kept in sync through the &lt;code&gt;rooms:updated&lt;/code&gt; webhook. It just never used it.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;Use the title when there is one, and keep the old wording when there is not, so older records without a stored title cannot end up with an empty name in bold:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;// fall back to &amp;quot;this space&amp;quot; for older records where title wasn&amp;#39;t yet stored&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; spaceName &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; publicspace&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.title &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;**&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;publicspace&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;.title&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;**&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;this space&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both reply variants (open space, and locked space where the bot is not a moderator) now use &lt;code&gt;spaceName&lt;/code&gt; instead of the literal text. After the change:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Only users in the domain(s) &lt;strong&gt;example.com&lt;/strong&gt; can join &lt;strong&gt;The Greatest Webex Team room ever!&lt;/strong&gt; using &lt;a href=&quot;https://eurl.io/#1A2B3C-AB&quot;&gt;https://eurl.io/#1A2B3C-AB&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No schema change and no extra API call: the data was already there.&lt;/p&gt;
&lt;h2 id=&quot;what-i-could-not-test&quot;&gt;What I could not test&lt;/h2&gt;
&lt;p&gt;The repository has no test suite, and running the bot needs a live MongoDB instance and a real bot token, which I did not have. The &lt;a href=&quot;https://github.com/webex/eurl/pull/16&quot;&gt;pull request&lt;/a&gt; says so plainly and asks the maintainers to check it against a real space, including one with a blank title to exercise the fallback. It is still open.&lt;/p&gt;
&lt;aside class=&quot;callout callout--tip&quot; aria-label=&quot;tip&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;tip:&lt;span&gt; small fixes, real users&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;This is a four-line change, but it removes a question every reader of a pasted join link would otherwise have to ask: “which space?”&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;</content:encoded><category>webex</category><category>javascript</category><category>open-source</category></item><item><title>An interactive WireGuard server installer</title><link>https://cenko.tech/blog/wireguard-installer-script/</link><guid isPermaLink="true">https://cenko.tech/blog/wireguard-installer-script/</guid><description>A shell script that installs a WireGuard server, sets up forwarding and NAT, opens the firewall port and creates client configs with QR codes.</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;a href=&quot;https://github.com/toor11/scripts&quot;&gt;toor11/scripts&lt;/a&gt; repository is a collection of system administration scripts. It had an &lt;code&gt;openvpn-install.sh&lt;/code&gt; but nothing for WireGuard, and its dozen scripts had no index. My &lt;a href=&quot;https://github.com/toor11/scripts/pull/1&quot;&gt;pull request&lt;/a&gt; added both: a README describing every script, and &lt;code&gt;wireguard-install.sh&lt;/code&gt;, written in the same interactive style as the OpenVPN installer.&lt;/p&gt;
&lt;h2 id=&quot;what-the-script-does&quot;&gt;What the script does&lt;/h2&gt;
&lt;p&gt;Run it as root:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; ./wireguard-install.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first run asks a few questions, each with a default you can accept: the server’s public IP or hostname, the UDP port (&lt;code&gt;51820&lt;/code&gt;), the public network interface, the tunnel subnet (&lt;code&gt;10.66.66.0/24&lt;/code&gt;), the first client’s name and the DNS server to push to clients (&lt;code&gt;1.1.1.1&lt;/code&gt;). Then it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Detects the distribution and installs WireGuard and &lt;code&gt;qrencode&lt;/code&gt; with apt, dnf, yum or pacman.&lt;/li&gt;
&lt;li&gt;Generates the server key pair and writes &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Turns on IPv4 forwarding in &lt;code&gt;/etc/sysctl.d/99-wireguard-forward.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Adds NAT with iptables rules that come up and go down with the interface:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;PostUp&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; iptables -A FORWARD -i wg0 -j ACCEPT&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;PostDown&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; iptables -D FORWARD -i wg0 -j ACCEPT&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Opens the UDP port in ufw or firewalld, whichever is active.&lt;/li&gt;
&lt;li&gt;Starts and enables &lt;code&gt;wg-quick@wg0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Creates the first client: its own key pair plus a preshared key, a client config, and a QR code printed in the terminal for the WireGuard phone app.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every later run shows a menu instead: add a client, revoke a client, or uninstall. Adding or revoking a client reloads the WireGuard interface so the change applies straight away.&lt;/p&gt;
&lt;aside class=&quot;callout callout--warning&quot; aria-label=&quot;warning&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;warning:&lt;span&gt; read before you run&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;Like the OpenVPN installer next to it, this script changes firewall rules, kernel settings and services. Read it and try it on a test machine before running it on a server you care about.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;aside class=&quot;callout callout--note&quot; aria-label=&quot;note&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;note:&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;In the script above, &lt;code&gt;wg0&lt;/code&gt; and &lt;code&gt;eth0&lt;/code&gt; stand for the values you choose at the prompts.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;</content:encoded><category>homelab</category><category>networking</category><category>wireguard</category><category>bash</category></item><item><title>Forcing Windows 11 to install the full language pack</title><link>https://cenko.tech/blog/windows-11-full-language-pack/</link><guid isPermaLink="true">https://cenko.tech/blog/windows-11-full-language-pack/</guid><description>Changing the display language in Settings can leave half of Windows in the old language. Four PowerShell cmdlets install the full pack and apply it everywhere.</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Changing the display language in &lt;strong&gt;Settings → Time &amp;amp; language → Language &amp;amp; region&lt;/strong&gt; on Windows 11 sometimes installs only a simplified, partial language pack. Some of the interface switches over, but many system elements, menus and dialogs stay in the old language, even after several restarts.&lt;/p&gt;
&lt;p&gt;I hit this when switching a Polish install of Windows 11 to English. The same thing can happen when you add a language, such as &lt;code&gt;fr-FR&lt;/code&gt;, to a system that was installed from an ISO in a different language.&lt;/p&gt;
&lt;h2 id=&quot;the-symptom&quot;&gt;The symptom&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Some UI elements switch to the new language.&lt;/li&gt;
&lt;li&gt;Many system menus and dialogs stay in the old one.&lt;/li&gt;
&lt;li&gt;Restarting does not help, no matter how many times.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;One PowerShell script, run as Administrator. Set &lt;code&gt;$targetLanguage&lt;/code&gt; to the language you want, for example &lt;code&gt;en-US&lt;/code&gt;, &lt;code&gt;en-GB&lt;/code&gt;, &lt;code&gt;pl-PL&lt;/code&gt;, &lt;code&gt;fr-FR&lt;/code&gt; or &lt;code&gt;pt-BR&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# --- Variables ---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;$targetLanguage &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &amp;quot;en-US&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; # Change to en-GB if you prefer British English&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# --- Install Language Pack ---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Install-Language&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;Language $targetLanguage&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# --- Set User Display Language ---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Set-WinUserLanguageList&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; $targetLanguage &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;Force&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# --- Copy Settings to System and Welcome Screen ---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Copy-UserInternationalSettingsToSystem&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;WelcomeScreen &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;$True&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;NewUser &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;$True&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# --- Force a Restart to Apply Changes ---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Restart-Computer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;Force&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;aside class=&quot;callout callout--warning&quot; aria-label=&quot;warning&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;warning:&lt;span&gt; it restarts without asking&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;The last line restarts the computer immediately, with no confirmation. Save your work before you run it.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;h2 id=&quot;what-each-step-does&quot;&gt;What each step does&lt;/h2&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table&quot;&gt;&lt;table class=&quot;stack-sm&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cmdlet&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Cmdlet&quot;&gt;&lt;code&gt;Install-Language&lt;/code&gt;&lt;/td&gt;
&lt;td data-label=&quot;What it does&quot;&gt;Forces the &lt;strong&gt;full&lt;/strong&gt; language pack instead of the simplified one Settings installs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Cmdlet&quot;&gt;&lt;code&gt;Set-WinUserLanguageList&lt;/code&gt;&lt;/td&gt;
&lt;td data-label=&quot;What it does&quot;&gt;Makes the target language the active one for the current user.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Cmdlet&quot;&gt;&lt;code&gt;Copy-UserInternationalSettingsToSystem&lt;/code&gt;&lt;/td&gt;
&lt;td data-label=&quot;What it does&quot;&gt;Copies the language and regional settings to system processes, the Welcome and lock screen, and new user accounts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Cmdlet&quot;&gt;&lt;code&gt;Restart-Computer -Force&lt;/code&gt;&lt;/td&gt;
&lt;td data-label=&quot;What it does&quot;&gt;Restarts straight away so the change applies everywhere.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Copy-UserInternationalSettingsToSystem&lt;/code&gt; needs both &lt;code&gt;-WelcomeScreen&lt;/code&gt; and &lt;code&gt;-NewUser&lt;/code&gt;. Leave one out and PowerShell stops to ask for it. Set &lt;code&gt;-NewUser $False&lt;/code&gt; if future accounts should keep the old language.&lt;/p&gt;
&lt;h2 id=&quot;good-to-know&quot;&gt;Good to know&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;It needs &lt;strong&gt;Windows 11 22H2 or later&lt;/strong&gt;, because that is where &lt;code&gt;Install-Language&lt;/code&gt; first appeared. It also needs an internet connection and an Administrator session.&lt;/li&gt;
&lt;li&gt;The install can look stuck for up to about 20 minutes. It is not. Let it run.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set-WinUserLanguageList&lt;/code&gt; replaces your whole language list. To keep another language installed for typing, pass an array instead:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Set-WinUserLanguageList&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; @&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;en-US&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;pl-PL&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;Force&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;aside class=&quot;callout callout--tip&quot; aria-label=&quot;tip&quot;&gt;&lt;p class=&quot;callout__label&quot;&gt;tip:&lt;span&gt; if the script is blocked&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;callout__body&quot;&gt;&lt;p&gt;If PowerShell refuses to run scripts, lift the execution policy for this session only, then run the script again:&lt;/p&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Set-ExecutionPolicy&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;Scope &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;Process&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;ExecutionPolicy Bypass&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;If &lt;code&gt;Install-Language&lt;/code&gt; is not recognised on your build, the fallback is &lt;code&gt;Add-WindowsCapability&lt;/code&gt;, adjusting the language tag as needed:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground);overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Add-WindowsCapability&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;Online &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;Name &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&amp;quot;Language.Basic~~~en-US~0.0.1.0&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script and the full instructions are in the &lt;a href=&quot;https://github.com/0xgitpushpray/Win-11-Language-Pack-Fix&quot;&gt;repository on GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>windows</category><category>powershell</category><category>troubleshooting</category></item></channel></rss>