[{"content":"This setup is a hands-free way to keep your Flatpak applications updated automatically. With the timer and service file, you don\u0026rsquo;t have to worry about manually checking for updates, as everything is done for you at regular intervals.\nTimer Unit: tolga.timer Create file: sudo nano ~/.config/systemd/user/tolga.timer\n[Unit] Description=Run Tolga\u0026#39;s Service Every 5 Minutes VER:2.0A [Timer] OnBootSec=1min OnUnitActiveSec=5min Unit=tolga.service [Install] WantedBy=timers.target This timer is responsible for triggering the service that handles Flatpak updates.\nOnBootSec=1min: The timer will wait 1 minute after the system boots up before it starts the service.\nOnUnitActiveSec=5min: After the service is activated, the timer will trigger it every 5 minutes. This ensures the service is checked and run repeatedly.\nUnit=tolga.service: This tells the timer which service to start — the service named tolga.service.\nWhy is this useful? My timer ensures that the Flatpak update process runs automatically in the background, without needing manual intervention. This is a hands-free way of making sure your Flatpak apps are always up to date.\nService Unit: tolga.service Create file: sudo nano ~/.config/systemd/user/tolga.service\n[Unit] Description=Tolga\u0026#39;s Flatpak Automatic Update and Notification VER:2.0A Documentation=man:flatpak(1) Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecCondition=/bin/bash -c \u0026#39;[[ \u0026#34;$(busctl get-property org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.NetworkManager Metered | cut -c 3-)\u0026#34; == @(2[4] )]]\u0026#39; ExecStart=/usr/bin/flatpak --system uninstall --unused -y --noninteractive ; /usr/bin/flatpak --system update -y --noninteractive ; /usr/bin/flatpak --system repair ; /usr/bin/notify-send \u0026#34;Flatpaks Updated\u0026#34; \u0026#34;Your computer is ready!\u0026#34; -app-name=\u0026#34;Flatpak Update Service\u0026#34; -u NORMAL TimeoutStopFailureMode=abort Environment=SYSTEMD_SLEEP_FREEZE_USER_SESSIONS=0 This service contains the commands to update and notify you about Flatpak updates.\nType=oneshot: The service runs once, completes its task, and then stops.\nExecCondition: This checks whether the network is metered (like on mobile networks) and prevents updates from running if the connection is metered.\nExecStart: This is where the updates actually happen:\nUninstalls unused Flatpak apps Updates all Flatpak apps Repairs any issues with Flatpak installations Sends a notification when everything is done TimeoutStopFailureMode=abort: If the service doesn\u0026rsquo;t stop correctly, it will abort and help you troubleshoot.\nEnvironment=SYSTEMD_SLEEP_FREEZE_USER_SESSIONS=0: Ensures the service won\u0026rsquo;t be interrupted during sleep mode.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/auto-update-flatpak-updater/","summary":"\u003cp\u003eThis setup is a hands-free way to keep your Flatpak applications updated automatically. With the timer and service file, you don\u0026rsquo;t have to worry about manually checking for updates, as everything is done for you at regular intervals.\u003c/p\u003e\n\u003ch2 id=\"timer-unit-tolgatimer\"\u003eTimer Unit: tolga.timer\u003c/h2\u003e\n\u003cp\u003eCreate file: \u003ccode\u003esudo nano ~/.config/systemd/user/tolga.timer\u003c/code\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[Unit]\nDescription=Run Tolga\u0026#39;s Service Every 5 Minutes VER:2.0A\n\n[Timer]\nOnBootSec=1min\nOnUnitActiveSec=5min\nUnit=tolga.service\n\n[Install]\nWantedBy=timers.target\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThis timer is responsible for triggering the service that handles Flatpak updates.\u003c/p\u003e","title":"Tolga Systemd Timer and Service for Flatpak Updates"},{"content":"Why This Script Exists Every time I write a new blog post, Hugo has to rebuild the entire site. That means recompiling all the markdown, regenerating all the HTML, copying files to the web root where nginx can serve them, and fixing Linux file permissions so nginx can actually read them.\nDoing all that manually is tedious. Running separate commands every time is error-prone. I wanted one command that does everything.\nSo I created hugo-deploy.\nWhat The Script Does (Simply) The script does five things in order.\n1. Check you are root\nThe script needs to write to system directories like /var/www/. Only root can do that. If you try to run it as a regular user, it exits with an error and tells you to use sudo.\n2. Figure out who you really are\nWhen you run sudo hugo-deploy, the script is running as root, but it needs to know who the real user is (me, tolga). It reads the SUDO_USER environment variable to find out. Then it gets my home directory from /etc/passwd.\nWhy? Because Hugo needs to build as the real user, not as root. If Hugo runs as root, it creates files owned by root, and that causes permission problems later.\n3. Build the Hugo site\nHugo reads all my markdown posts, generates HTML, and puts it in the public/ folder. The script runs this command:\nhugo --minify The --minify flag makes the HTML and CSS smaller so pages load faster. If Hugo fails at this step, the script stops and doesn\u0026rsquo;t deploy anything. Safety first.\n4. Deploy to the web root\nThe web root is where nginx looks for files to serve. I set it to /var/www/kingtolga/. The script does:\nDelete everything in the old web root Copy all the new HTML from Hugo\u0026rsquo;s public/ folder Change ownership to nginx:nginx so nginx can read the files Run restorecon to fix SELinux labels (Fedora\u0026rsquo;s security system) Without the ownership and SELinux step, nginx wouldn\u0026rsquo;t be able to read the files and would return permission denied errors.\n5. Verify the site is actually live\nThe script waits one second, then tries to curl the site on localhost:80. If it gets a 200 OK response, it prints success. If it gets nothing, it prints the command you need to run to debug it.\nWhy I Put It in /usr/local/bin/ I could have put the script anywhere. I chose /usr/local/bin/ for specific reasons.\n/usr/local/bin/ is in the system PATH\nWhen you type a command in the terminal, the shell searches a list of directories (called PATH) to find the executable. /usr/local/bin/ is always in that PATH. That means I can run sudo hugo-deploy from anywhere on the system without typing the full path. If the script was in /root/ or /opt/ or my home directory, I would have to type the full path every time.\n/usr/local/ survives package updates\nFedora and other Linux distros own /usr/bin/. When you update packages, they can add or replace files in /usr/bin/. But /usr/local/ is reserved for the system administrator (that\u0026rsquo;s me). Package managers never touch it. So my script will never get overwritten or deleted by an update.\n/usr/local/ is the Linux standard for local scripts\nThe Filesystem Hierarchy Standard (FHS) says:\n\u0026ldquo;Binaries in /usr/local/bin/ are meant for programs that are not managed by the package manager.\u0026rdquo;\nThat\u0026rsquo;s exactly what hugo-deploy is. It\u0026rsquo;s a custom script I wrote, not something from Fedora\u0026rsquo;s package manager. Putting it in /usr/local/bin/ follows the standard and makes my system predictable.\n/usr/local/bin/ feels like the right place\nWhen I ls /usr/local/bin/, I see custom tools. My backup scripts live there. Any utility I write goes there. It\u0026rsquo;s like a toolbox. System packages go in /usr/bin/ (owned by the distro), and my tools go in /usr/local/bin/ (owned by me).\nHow I Set It Up I copied the script to /usr/local/bin/ and made it executable:\nsudo cp hugo-deploy.sh /usr/local/bin/hugo-deploy sudo chmod 755 /usr/local/bin/hugo-deploy Note that I removed the .sh extension. On Unix systems, executables usually don\u0026rsquo;t have file extensions. The extension is just for humans to know what language it\u0026rsquo;s written in. The system doesn\u0026rsquo;t care.\nNow I can deploy by typing:\nsudo hugo-deploy From anywhere on T620. No path needed. No .sh extension. Just the command.\nThe Script Itself Here\u0026rsquo;s what it looks like:\n#!/usr/bin/env bash set -uo pipefail GREEN=\u0026#39;\\033[0;32m\u0026#39;; YELLOW=\u0026#39;\\033[1;33m\u0026#39;; RED=\u0026#39;\\033[0;31m\u0026#39;; NC=\u0026#39;\\033[0m\u0026#39; info() { echo -e \u0026#34;${YELLOW}[*]${NC} $1\u0026#34;; } ok() { echo -e \u0026#34;${GREEN}[OK]${NC} $1\u0026#34;; } err() { echo -e \u0026#34;${RED}[!]${NC} $1\u0026#34;; } if [[ $EUID -ne 0 ]]; then err \u0026#34;Run as root: sudo $0\u0026#34; exit 1 fi REAL_USER=\u0026#34;${SUDO_USER:-tolga}\u0026#34; REAL_HOME=$(getent passwd \u0026#34;$REAL_USER\u0026#34; | cut -d: -f6) HUGO_ROOT=\u0026#34;${REAL_HOME}/hugo/kingtolga\u0026#34; WEB_ROOT=\u0026#34;/var/www/kingtolga\u0026#34; NGINX_PORT=\u0026#34;80\u0026#34; if [[ ! -d \u0026#34;$HUGO_ROOT\u0026#34; ]]; then err \u0026#34;No Hugo site found at ${HUGO_ROOT} — run install-hugo-site.sh first\u0026#34; exit 1 fi info \u0026#34;Building site...\u0026#34; su \u0026#34;$REAL_USER\u0026#34; -c \u0026#34;cd \u0026#39;$HUGO_ROOT\u0026#39; \u0026amp;\u0026amp; hugo --minify\u0026#34; \\ \u0026amp;\u0026amp; ok \u0026#34;Build succeeded\u0026#34; \\ || { err \u0026#34;hugo build failed — nothing was deployed\u0026#34;; exit 1; } info \u0026#34;Deploying to ${WEB_ROOT}...\u0026#34; rm -rf \u0026#34;${WEB_ROOT:?}\u0026#34;/* cp -r \u0026#34;${HUGO_ROOT}/public/\u0026#34;* \u0026#34;$WEB_ROOT/\u0026#34; chown -R nginx:nginx \u0026#34;$WEB_ROOT\u0026#34; restorecon -Rv \u0026#34;$WEB_ROOT\u0026#34; \u0026gt; /dev/null ok \u0026#34;Deployed\u0026#34; info \u0026#34;Verifying...\u0026#34; sleep 1 if curl -sf \u0026#34;http://127.0.0.1:${NGINX_PORT}/\u0026#34; \u0026gt; /dev/null; then ok \u0026#34;Site is live and responding locally\u0026#34; else err \u0026#34;Local check failed — run: curl -I http://127.0.0.1:${NGINX_PORT}/\u0026#34; fi The set -uo pipefail line makes bash strict. It exits immediately if any command fails. That prevents me from accidentally deploying a broken build.\nThe su \u0026quot;$REAL_USER\u0026quot; -c line switches to the real user to build Hugo, then switches back to root to deploy. This is important for file ownership.\nWhy This Matters Before I had this script, deploying a new post took five commands:\ncd ~/hugo/kingtolga hugo --minify sudo rm -rf /var/www/kingtolga/* sudo cp -r public/* /var/www/kingtolga/ sudo chown -R nginx:nginx /var/www/kingtolga If I forgot one step, the site might not update. If I typo\u0026rsquo;d a path, bad things could happen. If nginx lost permissions, the site would 404.\nNow I type one command:\nsudo hugo-deploy The script does all five things automatically, in the right order, with safety checks. If anything fails, it tells me what went wrong instead of silently breaking things.\nThat\u0026rsquo;s the whole point of good infrastructure. Automate the routine. Make it so simple that you can\u0026rsquo;t get it wrong.\nThe Lesson Small scripts in the right places save time and prevent mistakes. /usr/local/bin/ is where they belong on a Linux system.\nNext time you find yourself typing the same sequence of commands over and over, turn it into a script. Put it in /usr/local/bin/. Make it executable. Stop typing the long version.\nThat\u0026rsquo;s how you build a system that works for you instead of against you.\n-Tolga\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/hugo-deploy-script-breakdown/","summary":"\u003ch2 id=\"why-this-script-exists\"\u003eWhy This Script Exists\u003c/h2\u003e\n\u003cp\u003eEvery time I write a new blog post, Hugo has to rebuild the entire site. That means recompiling all the markdown, regenerating all the HTML, copying files to the web root where nginx can serve them, and fixing Linux file permissions so nginx can actually read them.\u003c/p\u003e\n\u003cp\u003eDoing all that manually is tedious. Running separate commands every time is error-prone. I wanted one command that does everything.\u003c/p\u003e","title":"My hugo-deploy: A Simple Script That Keeps My Blog Running"},{"content":"The Fuckup I wanted to migrate from Hugo to WordPress. Seemed simple. It wasn\u0026rsquo;t.\nI spent hours trying to install WordPress on my T620 thin client. Each time I \u0026ldquo;fixed\u0026rdquo; it, something else broke. By the end, both WordPress AND my Hugo site were completely fucked. Nginx wouldn\u0026rsquo;t start. The main blog was down. I had wasted hours on something that should have taken 30 minutes.\nThen I remembered: I had full backups.\nWithout them, I would have lost everything.\nWhat Went Wrong The problem was trying to run nginx with conflicting server blocks. The main /etc/nginx/nginx.conf had a default server listening on port 80. My WordPress config tried to add another. Then I tried disabling the default. Then I accidentally commented out the include statements that load /etc/nginx/conf.d/.\nEach fix made it worse. Nginx wouldn\u0026rsquo;t even syntax-check anymore.\nThe real lesson: never try to fix infrastructure without backups in place first.\nThe Backup Setup I Should Have Used From The Start Here\u0026rsquo;s what saved me.\nCron Job: Automated Weekly Backups (T620 → G4) Every Sunday at 3am, a cron job runs on T620 and backs up:\n/etc/nginx/ - all web configs /etc/systemd/system/ - all systemd services ~/hugo/kingtolga/ - the entire Hugo site /var/www/kingtolga/ - the compiled HTML Old backups older than 28 days auto-delete.\nSet up the cron job on T620:\necho \u0026#39;0 3 * * 0 root cp /etc/fstab /root/backup-staging/fstab \u0026amp;\u0026amp; tar -czf /root/backup-$(date +\\%Y\\%m\\%d).tar.gz /root/backup-staging /etc/ssh /etc/samba /etc/nginx /etc/systemd/system /home/tolga/hugo /var/www \u0026amp;\u0026amp; chmod 600 /root/backup-$(date +\\%Y\\%m\\%d).tar.gz \u0026amp;\u0026amp; find /root -maxdepth 1 -name \u0026#34;backup-*.tar.gz\u0026#34; -mtime +28 -delete\u0026#39; | sudo tee /etc/cron.d/config-backup Verify it\u0026rsquo;s set:\ncat /etc/cron.d/config-backup sudo systemctl status crond Manual Backup Command (Run Anytime) If you want to backup right now without waiting for Sunday:\nsudo cp /etc/fstab /root/backup-staging/fstab sudo tar -czf /root/backup-$(date +%Y%m%d).tar.gz /root/backup-staging /etc/ssh /etc/samba /etc/nginx /etc/systemd/system /home/tolga/hugo /var/www sudo chmod 600 /root/backup-$(date +%Y%m%d).tar.gz Verify:\nsudo ls -lah /root/backup-*.tar.gz sudo tar -tzf /root/backup-$(date +%Y%m%d).tar.gz | head -30 Copy Backups to G4 (From T620) Every backup sits on T620. But if T620 dies, you\u0026rsquo;ve got nothing. Copy to G4:\nsudo mkdir -p /root/backup-staging sudo scp /root/backup-*.tar.gz tolga@192.168.0.xxx:/home/tolga/HUGO-BACKUP/ Verify on G4:\nls -lah ~/HUGO-BACKUP/ Systemd Services for Automatic Backups If you want automated backups via systemd instead of cron:\nCreate /etc/systemd/system/backup.timer:\nsudo bash \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; cat \u0026gt; /etc/systemd/system/backup.timer \u0026lt;\u0026lt; \u0026#39;TIMER\u0026#39; [Unit] Description=My weekly backup timer Requires=backup.service [Timer] OnBootSec=5min OnUnitActiveSec=1w Persistent=true [Install] WantedBy=timers.target TIMER cat \u0026gt; /etc/systemd/system/backup.service \u0026lt;\u0026lt; \u0026#39;SERVICE\u0026#39; [Unit] Description=T620 weekly backup to G4 After=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/backup-to-g4.sh [Install] WantedBy=multi-user.target SERVICE systemctl daemon-reload systemctl enable --now backup.timer systemctl status backup.timer EOF Create the backup script /usr/local/bin/backup-to-g4.sh:\nsudo bash \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; cat \u0026gt; /usr/local/bin/backup-to-g4.sh \u0026lt;\u0026lt; \u0026#39;SCRIPT\u0026#39; #!/bin/bash set -e backup_file=\u0026#34;/root/backup-$(date +%Y%m%d).tar.gz\u0026#34; # Copy fstab to staging cp /etc/fstab /root/backup-staging/fstab # Create archive tar -czf \u0026#34;$backup_file\u0026#34; \\ /root/backup-staging \\ /etc/ssh \\ /etc/samba \\ /etc/nginx \\ /etc/systemd/system \\ /home/tolga/hugo \\ /var/www chmod 600 \u0026#34;$backup_file\u0026#34; # Copy to G4 scp \u0026#34;$backup_file\u0026#34; tolga@192.168.0.xxx:/home/tolga/HUGO-BACKUP/ # Clean old backups (older than 28 days) find /root -maxdepth 1 -name \u0026#34;backup-*.tar.gz\u0026#34; -mtime +28 -delete echo \u0026#34;Backup complete: $backup_file\u0026#34; SCRIPT chmod 755 /usr/local/bin/backup-to-g4.sh EOF Enable and test:\nsudo systemctl start backup.timer sudo systemctl status backup.timer How to Restore From Backups When things go catastrophically wrong (like they did for me):\nRestore Everything on T620 from G4 Backups # On T620, pull backups from G4 cd ~ scp tolga@192.168.0.xxx:/home/tolga/HUGO-BACKUP/backup-*.tar.gz ~/ # Restore each piece sudo tar -xzf ~/backup-*.tar.gz -C / # Restart services sudo systemctl daemon-reload sudo systemctl restart nginx sudo systemctl restart tailscale-funnel # Verify Hugo site curl http://localhost # Verify Tailscale Funnel curl https://jacksparrow2.tail9e758e.ts.net/ Or Restore Just Specific Parts # Just restore Hugo tar -xzf ~/backup-*.tar.gz -C ~/ home/tolga/hugo/ # Just restore nginx sudo tar -xzf ~/backup-*.tar.gz -C / etc/nginx/ # Just restore systemd sudo tar -xzf ~/backup-*.tar.gz -C / etc/systemd/system/ Scripts to Keep Handy T620 → G4 Backup Script (t620-to-g4-backup.sh):\n#!/bin/bash # Run this anytime to backup T620 to G4 echo \u0026#34;Creating backup...\u0026#34; sudo cp /etc/fstab /root/backup-staging/fstab sudo tar -czf /root/backup-$(date +%Y%m%d).tar.gz \\ /root/backup-staging \\ /etc/ssh \\ /etc/samba \\ /etc/nginx \\ /etc/systemd/system \\ /home/tolga/hugo \\ /var/www sudo chmod 600 /root/backup-*.tar.gz echo \u0026#34;Sending to G4...\u0026#34; sudo scp /root/backup-*.tar.gz tolga@192.168.0.27:/home/tolga/HUGO-BACKUP/ echo \u0026#34;Done. Backups on G4:\u0026#34; ls -lah ~/HUGO-BACKUP/ G4 → T620 Restore Script (g4-to-t620-restore.sh):\n#!/bin/bash # Run this on T620 to restore from G4 echo \u0026#34;Pulling backups from G4...\u0026#34; cd ~ scp tolga@192.168.0.xxx:/home/tolga/HUGO-BACKUP/backup-*.tar.gz ./ echo \u0026#34;Restoring nginx...\u0026#34; sudo tar -xzf backup-*.tar.gz -C / etc/nginx echo \u0026#34;Restoring systemd services...\u0026#34; sudo tar -xzf backup-*.tar.gz -C / etc/systemd/system echo \u0026#34;Restoring Hugo site...\u0026#34; tar -xzf backup-*.tar.gz -C ~/ home/tolga/hugo echo \u0026#34;Restoring www...\u0026#34; sudo tar -xzf backup-*.tar.gz -C / var/www echo \u0026#34;Reloading systemd...\u0026#34; sudo systemctl daemon-reload sudo systemctl restart nginx sudo systemctl restart tailscale-funnel echo \u0026#34;Done. Testing site...\u0026#34; sleep 2 curl http://localhost Save these scripts to /usr/local/bin/ on T620 and G4, make them executable:\nsudo chmod 755 /usr/local/bin/t620-to-g4-backup.sh sudo chmod 755 /usr/local/bin/g4-to-t620-restore.sh The Lesson I broke my Hugo blog because I didn\u0026rsquo;t have a tested restore procedure in place before things went wrong.\nThe rule: If you don\u0026rsquo;t have a backup and a tested way to restore from it, you don\u0026rsquo;t have a backup.\nNow I do. Every Sunday at 3am, T620 backs itself up. Once a week manually, I push those backups to G4. If T620 explodes tomorrow, I restore in 15 minutes.\nNever skip backups. Never.\n-Tolga\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/wordpress-hugo-backup-disaster/","summary":"\u003ch2 id=\"the-fuckup\"\u003eThe Fuckup\u003c/h2\u003e\n\u003cp\u003eI wanted to migrate from Hugo to WordPress. Seemed simple. It wasn\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eI spent hours trying to install WordPress on my T620 thin client. Each time I \u0026ldquo;fixed\u0026rdquo; it, something else broke. By the end, both WordPress AND my Hugo site were completely fucked. Nginx wouldn\u0026rsquo;t start. The main blog was down. I had wasted hours on something that should have taken 30 minutes.\u003c/p\u003e\n\u003cp\u003eThen I remembered: I had full backups.\u003c/p\u003e","title":"WordPress Nearly Killed My Hugo Blog: A Backup Disaster Story"},{"content":"I spent the last month building what should have been a simple update checker for my G4 Fedora 44 box. Turned into a proper rabbit hole once I discovered the system was hanging after three days of suspend. Here\u0026rsquo;s what I learned.\nThe Problem I run a lot of machines. Rather than babysitting each one for updates, I wanted something like CachyOS has: a tray icon that checks hourly, shows a green dot when up to date, red when updates are pending, and handles the background automation so I never have to think about it.\nSounds simple, right? Well, I built it. And it broke my suspend after three days. System would hang on login screen, couldn\u0026rsquo;t get to Plasma, TTY only. Hard reboot was the only way out.\nThe Architecture The updater works as three separate pieces that talk to each other:\n1. The main script (bash, runs manually or via systemd) Does the actual work: dnf sync, flatpak update, cruft cleanup, kernel trimming.\n2. The checker (bash, runs hourly via systemd timer) Counts pending updates and writes the numbers to cache files. Also fires desktop notifications.\n3. The tray (Python, AppIndicator3, user-level systemd service) Reads the cache files every 5 seconds and shows the appropriate icon: yellow while checking, red if updates found, green if all clear.\nThey communicate through files in /var/cache/linuxtweaks/. No D-Bus complexity, no pipes, just files. Dead simple.\nThe Main Bash Script #!/usr/bin/env bash set -uo pipefail This runs the whole thing with strict error checking. Any unset variable or failed command exits immediately. Keeps you from silently corrupting your system.\nColours and output helpers nc=\u0026#39;\\033[0m\u0026#39; red=\u0026#39;\\033[0;31m\u0026#39; grn=\u0026#39;\\033[0;32m\u0026#39; ylw=\u0026#39;\\033[1;33m\u0026#39; msg() { echo -e \u0026#34;${blu}➤${nc} $*\u0026#34;; } ok() { echo -e \u0026#34;${grn}✔${nc} $*\u0026#34;; } warn() { echo -e \u0026#34;${ylw}⚠${nc} $*\u0026#34;; } err() { echo -e \u0026#34;${red}✖${nc} $*\u0026#34;; } I\u0026rsquo;ve been doing this for years across every script I write. Makes terminal output readable at a glance: blue arrow for progress, green checkmark for success, yellow warning, red error. The head() function adds visual section breaks.\nConfiguration block app_dir=\u0026#34;/usr/local/bin/LinuxTweaks\u0026#34; cache_dir=\u0026#34;/var/cache/linuxtweaks\u0026#34; log_file=\u0026#34;/var/log/linuxtweaks-fedora-updater.log\u0026#34; kernels_to_keep=2 Everything is parameterized. If paths ever change, you only update this one block. The checker, tray, timer, all reference these same values during install.\nThe do_install function This is where it gets interesting. The script is self-installing. You run:\nsudo /usr/local/bin/LinuxTweaks/fedora-updater.sh --install And it does all this in one shot: downloads the icon, installs the bash checker script, installs the Python tray, creates systemd units, creates the desktop entry. This self-installing pattern means you can literally just drop the script on a machine and run it once. Everything else is automated from there.\nThe Checker Script This runs every hour and writes status to cache files.\ndnf makecache -q 2\u0026gt;/dev/null || true dnf_updates=$(dnf check-update -q 2\u0026gt;/dev/null | grep -Ev \u0026#39;^\\$|^Last metadata\u0026#39;) dnf_count=$(echo -n \u0026#34;$dnf_updates\u0026#34; | grep -c \u0026#39;^[A-Za-z0-9]\u0026#39; || true) echo \u0026#34;$dnf_count\u0026#34; \u0026gt; \u0026#34;$cache_dir/dnf-count\u0026#34; It counts DNF updates by parsing the check-update output. The grep filters out header noise. Writes the count to a file that the tray reads. Same pattern for flatpak, kernel pending state, and reboot requirements. Each gets its own cache file.\nThen it fires a desktop notification if the count changed from last time. This is how you first find out updates are available without having to look at the tray.\nThe Critical Fix: Persistent=false This is where I almost lost it.\nThe original timer had Persistent=true. This means: if systemd misses a scheduled run because the system was asleep, it replays all the missed runs when it wakes up.\nOver three days of suspend, that\u0026rsquo;s roughly 72 missed hourly runs. On resume, all 72 try to fire at once. All 72 hit DNF simultaneously. DNF deadlocks on the package cache lock. System hangs. Login screen freezes.\nThe fix:\n[Timer] OnBootSec=2min OnUnitActiveSec=1h Persistent=false RandomizedDelaySec=2min Persistent=false means: don\u0026rsquo;t replay missed runs. Just start fresh.\nRandomizedDelaySec=2min means: add a random 0-2 minute delay to each run, so if the system does wake up and multiple checks try to run, they\u0026rsquo;re staggered instead of piling on at the same instant.\nThis took me three days to figure out. The error logs showed nothing. The system just quietly hung. No way to know the timer was the culprit without digging deep into systemd behavior.\nThe Tray: Python AppIndicator3 I chose AppIndicator3 because it\u0026rsquo;s what CachyOS uses, and because it speaks native D-Bus StatusNotifierItem. Unlike the old GTK StatusIcon, it doesn\u0026rsquo;t require XWayland on Wayland systems.\nGREEN = (0.16, 0.68, 0.32, 1.0) RED = (0.86, 0.16, 0.16, 1.0) YELLOW = (1.0, 0.84, 0.0, 1.0) The app composites coloured dots onto the icon. Green dot = up to date. Red dot = updates available. Yellow dot = currently checking.\nThe is_checking detection def is_checking(): \u0026#34;\u0026#34;\u0026#34;Check if fedora-update-check.sh OR fedora-updater.sh --full is running\u0026#34;\u0026#34;\u0026#34; try: result = subprocess.run([\u0026#34;pgrep\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;fedora-update-check.sh\u0026#34;], capture_output=True, timeout=1) if result.returncode == 0: return True result = subprocess.run([\u0026#34;pgrep\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;fedora-updater.sh.*--full\u0026#34;], capture_output=True, timeout=1) return result.returncode == 0 except Exception: return False This detects if either the hourly checker or a manual full update is running. If yes, show yellow.\nWhy does this matter? Without it, you never see the yellow state because the checker runs too fast. By the time the tray wakes up to refresh, checking is already done. The yellow dot would flash past and you\u0026rsquo;d never notice.\nThe fix was two-fold: detect the running process with pgrep, and refresh the tray\u0026rsquo;s display every 5 seconds instead of 30.\nREFRESH_MS = 5 * 1000 # 5 seconds Now you actually see yellow while checking happens.\nThe refresh loop def refresh(self): checking = is_checking() dnf_count = read_int(f\u0026#34;{CACHE_DIR}/dnf-count\u0026#34;) flatpak_count = read_int(f\u0026#34;{CACHE_DIR}/flatpak-count\u0026#34;) total = dnf_count + flatpak_count if checking: self.status_item.set_label(\u0026#34;🟡 Checking for updates...\u0026#34;) if self.has_checking_icon: self.indicator.set_icon_full(ICON_CHECKING, \u0026#34;Checking...\u0026#34;) elif total \u0026gt; 0: self.status_item.set_label(f\u0026#34;🔴 {total} update(s) available\u0026#34;) if self.has_alert_icon: self.indicator.set_icon_full(ICON_ALERT, \u0026#34;Updates available\u0026#34;) else: self.status_item.set_label(\u0026#34;🟢 System up to date\u0026#34;) if self.has_ok_icon: self.indicator.set_icon_full(ICON_OK, \u0026#34;Up to date\u0026#34;) return True Priority order: yellow (checking) beats red (updates) beats green (clean).\nThis runs every 5 seconds. It reads the cache files, checks if any update processes are running, and updates the menu label and icon accordingly.\nService Configuration: Timeout Protection [Service] Type=oneshot ExecStart=/usr/local/bin/LinuxTweaks/fedora-update-check.sh TimeoutStartSec=90 TimeoutStopSec=10 Here\u0026rsquo;s a lesson I learned the hard way: systemd services hang forever by default if the process doesn\u0026rsquo;t exit.\nIf DNF deadlocks or gets stuck, the checker will sit there forever waiting. The tray will show outdated status. Nothing moves.\nTimeoutStartSec=90 means: give the checker script 90 seconds to start and do its work. After 90s, force-kill it.\nTimeoutStopSec=10 means: when stopping, wait 10 seconds for clean shutdown. After that, SIGKILL.\nThis prevents one hung checker from cascading into system-wide issues.\nRunning Updates Manually sudo /usr/local/bin/LinuxTweaks/fedora-updater.sh --full This runs all four phases in sequence: do_dnf, do_autoremove, do_flatpak, do_cruft.\nYou can run individual phases too if you only want specific updates.\nafter_run: Immediate Green after_run() { head \u0026#34;Done\u0026#34; ok \u0026#34;Log: $log_file\u0026#34; # Reset ALL cache counters immediately echo 0 \u0026gt; \u0026#34;$cache_dir/update-count\u0026#34; echo 0 \u0026gt; \u0026#34;$cache_dir/dnf-count\u0026#34; echo 0 \u0026gt; \u0026#34;$cache_dir/flatpak-count\u0026#34; # Force immediate tray refresh if [[ -x \u0026#34;$checker_dest\u0026#34; ]]; then \u0026#34;$checker_dest\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 || true fi notify \u0026#34;System sync complete.\u0026#34; if [[ \u0026#34;$(cat \u0026#34;$cache_dir/reboot-required\u0026#34; 2\u0026gt;/dev/null)\u0026#34; == \u0026#34;1\u0026#34; ]]; then warn \u0026#34;A newer kernel was installed ... reboot when convenient.\u0026#34; else ok \u0026#34;No reboot required — tray is now showing green.\u0026#34; fi } This runs after all four update phases finish. It resets the counters to zero. This forces the tray to immediately turn green, because it\u0026rsquo;s reading the cache files every 5 seconds and will see the zero counts right away. No waiting 30 seconds for the next cycle.\nThe user sees: run updates, watch the tray, see it go red (there are updates), then green (all done).\nWhy This Matters Before I built this, I was manually checking for updates every few weeks. That\u0026rsquo;s how you end up running old software with known vulnerabilities. I wanted something that Just Works in the background, shows me status at a glance, and never breaks the system.\nThe three-day suspend hang taught me that automation at scale is dangerous if you don\u0026rsquo;t understand the semantics. Persistent=true seems innocent until your system has been asleep for three days and 72 tasks queue up.\nThe 5-second refresh interval and is_checking detection taught me that users want immediate feedback. Seeing the yellow dot while checking happens is satisfying. Without it, the system feels broken.\nThe cache file approach taught me that shared memory is simpler than D-Bus complexity. Three processes reading and writing files is more reliable than trying to coordinate signals and subscriptions.\nLessons Learned Persistent=true is dangerous for long-suspend systems. Always use Persistent=false if you have mobile devices or machines that sleep days at a time.\nDetect running processes to give real-time feedback. Users need to see yellow while checking happens, not guess.\nKeep refresh intervals short for interactive systems. 5 seconds feels alive. 30 seconds feels dead.\nUse timeouts on systemd services. One hung process shouldn\u0026rsquo;t bring the system down.\nCache files are simpler than message passing. Three bash processes writing to /var/cache is more robust than D-Bus coordination.\nSelf-installing scripts mean you never have to remember how to deploy them. One command sets everything up.\nThe updater has been running solid for two weeks now. No hangs, no deadlocks, no missed notifications. The suspend test is coming, but I\u0026rsquo;m confident the Persistent=false fix will hold.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/fedora-updater-system-v3/","summary":"\u003cp\u003eI spent the last month building what should have been a simple update checker for my G4 Fedora 44 box. Turned into a proper rabbit hole once I discovered the system was hanging after three days of suspend. Here\u0026rsquo;s what I learned.\u003c/p\u003e\n\u003ch2 id=\"the-problem\"\u003eThe Problem\u003c/h2\u003e\n\u003cp\u003eI run a lot of machines. Rather than babysitting each one for updates, I wanted something like CachyOS has: a tray icon that checks hourly, shows a green dot when up to date, red when updates are pending, and handles the background automation so I never have to think about it.\u003c/p\u003e","title":"Building a Fedora System Updater with Live Status Indicators"},{"content":"Most people manually update stuff when they remember. But you can make your system do it automatically with a couple of files.\nI was looking at keeping on top of flatpak, snap, and appimage updates and realised I just forget to do it. Found this solution using systemd timers, pretty straightforward.\nWhat we\u0026rsquo;re building Two files that work together:\nA bash script that runs the actual updates A systemd timer that says when to run it That\u0026rsquo;s it. No daemons running constantly, no cron job headaches, just systemd doing what it\u0026rsquo;s built for.\nStep 1: Create the update script Open terminal and run this to create the file:\nsudo nano /usr/local/bin/auto-app-updates.sh Paste this into nano:\n#!/usr/bin/env bash set -e echo \u0026#34;==\u0026gt; starting app updates at $(date)\u0026#34; # flatpak if command -v flatpak \u0026amp;\u0026gt; /dev/null; then echo \u0026#34;updating flatpak apps...\u0026#34; flatpak update -y echo \u0026#34;flatpak done\u0026#34; fi # snap if command -v snap \u0026amp;\u0026gt; /dev/null; then echo \u0026#34;updating snap...\u0026#34; snap refresh echo \u0026#34;snap done\u0026#34; fi # appimage (if you have a standard location for them) if [ -d ~/AppImages ]; then echo \u0026#34;checking appimages...\u0026#34; for appimage in ~/AppImages/*.AppImage; do if [ -f \u0026#34;$appimage\u0026#34; ]; then echo \u0026#34;found: $appimage\u0026#34; fi done echo \u0026#34;appimage check done\u0026#34; fi echo \u0026#34;==\u0026gt; app updates finished at $(date)\u0026#34; Save and exit nano: Ctrl+X, then Y, then Enter.\nMake it executable:\nsudo chmod +x /usr/local/bin/auto-app-updates.sh Test it works:\nsudo /usr/local/bin/auto-app-updates.sh Step 2: Create the systemd service Run this:\nsudo nano /etc/systemd/system/auto-app-updates.service Paste this:\n[Unit] Description=Auto-update flatpak snap and appimage After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/auto-app-updates.sh StandardOutput=journal StandardError=journal Save and exit: Ctrl+X, Y, Enter.\nStep 3: Create the systemd timer Run this:\nsudo nano /etc/systemd/system/auto-app-updates.timer Paste this:\n[Unit] Description=Run app updates daily Requires=auto-app-updates.service [Timer] OnCalendar=daily OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target Save and exit: Ctrl+X, Y, Enter.\nThis runs at 2am every day. Change 02:00:00 to whatever time you want (24-hour format).\nStep 4: Enable and start Run these commands:\nsudo systemctl daemon-reload sudo systemctl enable auto-app-updates.timer sudo systemctl start auto-app-updates.timer Check it\u0026rsquo;s running:\nsudo systemctl status auto-app-updates.timer See when it runs next:\nsystemctl list-timers auto-app-updates.timer That\u0026rsquo;s it Your system will now automatically update flatpak, snap, and appimage apps every day at 2am.\nVerify it worked Check the logs after it runs:\nsudo journalctl -u auto-app-updates.service -n 20 Change the schedule To run at a different time, edit the timer:\nsudo nano /etc/systemd/system/auto-app-updates.timer Change this line:\nOnCalendar=*-*-* 02:00:00 Then reload:\nsudo systemctl daemon-reload sudo systemctl restart auto-app-updates.timer Only want flatpak or snap? Edit the script and comment out or delete what you don\u0026rsquo;t need:\nsudo nano /usr/local/bin/auto-app-updates.sh Remove the flatpak section, snap section, or appimage section. Save and done.\nTroubleshooting Check logs for errors:\nsudo journalctl -u auto-app-updates.service Check if timer is active:\nsudo systemctl status auto-app-updates.timer Make sure filenames are exactly right:\n/etc/systemd/system/auto-app-updates.service /etc/systemd/system/auto-app-updates.timer /usr/local/bin/auto-app-updates.sh ","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/systemd-timer-app-updates/","summary":"\u003cp\u003eMost people manually update stuff when they remember. But you can make your system do it automatically with a couple of files.\u003c/p\u003e\n\u003cp\u003eI was looking at keeping on top of flatpak, snap, and appimage updates and realised I just forget to do it. Found this solution using systemd timers, pretty straightforward.\u003c/p\u003e\n\u003ch2 id=\"what-were-building\"\u003eWhat we\u0026rsquo;re building\u003c/h2\u003e\n\u003cp\u003eTwo files that work together:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eA bash script that runs the actual updates\u003c/li\u003e\n\u003cli\u003eA systemd timer that says when to run it\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThat\u0026rsquo;s it. No daemons running constantly, no cron job headaches, just systemd doing what it\u0026rsquo;s built for.\u003c/p\u003e","title":"basic systemd timer to auto-update flatpak snap and appimage"},{"content":"Every time a new kernel arrives via dnf upgrade, your Fedora EFI boot entry disappears. You reboot and the UEFI firmware can\u0026rsquo;t find Fedora anymore. You have to manually regenerate GRUB and recreate the boot entry. Again.\nThis is a Fedora issue with duplicate or malformed boot entries in the UEFI firmware. The kernel update process has hooks that regenerate GRUB, but if you have bad entries sitting there, the system gets confused and deletes the good one instead of refreshing it.\nunderstanding the problem Your UEFI firmware has a boot entry list. When Fedora installs, it creates entries that point to shimx64.efi (the bootloader). If something goes wrong during install or updates, you can end up with multiple Fedora entries, some pointing to grubx64.efi directly, some pointing to shimx64.efi. Some might even point to partitions that don\u0026rsquo;t exist anymore.\nWhen a kernel update runs grub2-mkconfig and grub2-install, those tools look at the boot entries. If they see duplicates or malformed entries, they get paranoid and start deleting things to clean up. Sometimes they delete the good entry by mistake.\nchecking your current state First, see what boot entries you actually have:\nsudo efibootmgr -v | grep -i fedora If you see multiple Fedora entries, that is the problem. You should only have ONE. Boot0006 in my case, which points to shimx64.efi, not grubx64.efi.\nAlso check if your EFI partition is mounted in fstab:\ngrep /boot/efi /etc/fstab Should show something like:\nUUID=87A5-9EED /boot/efi vfat defaults,uid=0,gid=0,umask=0077,shortname=winnt 0 2 If it has noauto, change it to defaults so the EFI partition stays mounted.\nmy situation I had this:\nBoot0001* \\EFI\\fedora\\grubx64.efi pointing to sda1 Boot0002* \\EFI\\fedora\\grubx64.efi pointing to a UUID that doesn\u0026#39;t exist Boot0006* Fedora pointing to shimx64.efi on sda1 Boot0001 and Boot0002 were duplicates created during installs. They pointed directly to grubx64.efi instead of shimx64.efi. When kernel updates ran, GRUB saw three entries, got confused, and deleted or broke them.\nthe fix Three steps. Delete the bad entries, install a kernel hook, set the boot order.\nstep 1 - delete duplicate entries First, identify which ones to delete. grubx64.efi directly is wrong. shimx64.efi is correct. Delete anything that points to grubx64.efi without shimx64.efi in the path.\nsudo efibootmgr --bootnum 1 --delete-bootnum sudo efibootmgr --bootnum 2 --delete-bootnum Replace 1 and 2 with whatever your bad entries are. Verify they are gone:\nsudo efibootmgr -v | grep -i fedora Should show only ONE entry now. Mine is Boot0006.\nstep 2 - install a kernel post-install hook This is the key. Every time a kernel updates, Fedora runs scripts in /etc/kernel/postinst.d/. You can add your own hook there that regenerates GRUB after the update.\nCreate the directory if it does not exist:\nsudo mkdir -p /etc/kernel/postinst.d Now create the hook script:\nsudo bash -c \u0026#39;cat \u0026gt; /etc/kernel/postinst.d/99-grub-regenerate \u0026lt;\u0026lt; \u0026#34;EOF\u0026#34; #!/bin/bash /usr/sbin/grub2-mkconfig -o /boot/grub2/grub.cfg /usr/sbin/grub2-install --efi-directory=/boot/efi --force /dev/sda EOF chmod +x /etc/kernel/postinst.d/99-grub-regenerate \u0026#39; What this does. After a kernel install finishes, this script runs. It regenerates the GRUB config file with the new kernel, then reinstalls the bootloader to the EFI partition. The \u0026ndash;force flag bypasses paranoia about Secure Boot.\nVerify it was created:\nls -la /etc/kernel/postinst.d/ cat /etc/kernel/postinst.d/99-grub-regenerate step 3 - set boot order Make sure your good Fedora entry is first in the boot order:\nsudo efibootmgr --bootorder 0006,0000,0003,0004,0005 Replace 0006 with your actual Fedora boot entry number. Replace the rest with whatever else you have (USB, NVMe, DVD, etc). Verify:\nsudo efibootmgr -v | head -5 Should show your Fedora entry (0006 or whatever) as first.\nwhy this works Before: duplicate entries exist, kernel update runs GRUB tools, tools see multiple entries and get confused, they delete something or fail to regenerate, you lose your boot entry.\nAfter: only one correct entry exists, kernel update runs GRUB tools, tools see one entry and regenerate it properly, the hook makes sure GRUB is reinstalled to the EFI partition, your entry sticks through every update.\nThe hook is the permanent fix. It ensures that no matter what GRUB does during a kernel update, the bootloader gets reinstalled and the entry stays valid.\ntesting it When the next kernel update comes, run:\nsudo dnf upgrade Let it install. After it finishes, reboot:\nsudo systemctl reboot Fedora should boot normally. Then check:\nsudo efibootmgr -v | grep -i fedora Your entry should still be there. If it is, the hook worked and your entries will stick from now on.\nwhy you have to do this Fedora\u0026rsquo;s default install creates boot entries. If you have other distros on other drives or if something goes wrong during install, you can end up with duplicates or malformed entries. The kernel update process assumes entries are clean. If they are not, things break.\nThe hook fixes it by making sure GRUB is regenerated and reinstalled after every update. It is not a Fedora default but it should be. This is why I keep notes and document it.\ntakeaway Duplicate UEFI boot entries cause GRUB to break during kernel updates. Delete the bad ones. Install a kernel post-install hook to regenerate and reinstall GRUB after every update. Set the correct entry first in boot order.\nAfter that, your Fedora entry stays put through every kernel update you throw at it.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/fedora-grub-entries-disappearing-kernel-updates/","summary":"\u003cp\u003eEvery time a new kernel arrives via dnf upgrade, your Fedora EFI boot entry disappears. You reboot and the UEFI firmware can\u0026rsquo;t find Fedora anymore. You have to manually regenerate GRUB and recreate the boot entry. Again.\u003c/p\u003e\n\u003cp\u003eThis is a Fedora issue with duplicate or malformed boot entries in the UEFI firmware. The kernel update process has hooks that regenerate GRUB, but if you have bad entries sitting there, the system gets confused and deletes the good one instead of refreshing it.\u003c/p\u003e","title":"fedora grub entries disappearing after kernel updates, here's why and how to fix it"},{"content":"Reboot the system. It hangs. Doesn\u0026rsquo;t crash, doesn\u0026rsquo;t give an error on screen, just sits there for what feels like forever. Eventually it times out and reboots anyway. But every single reboot does this.\nCheck the logs and you see this:\nplasma-plasmashell.service: State \u0026#39;stop-sigterm\u0026#39; timed out. Aborting. plasma-plasmashell.service: Failed with result \u0026#39;timeout\u0026#39;. KDE Plasma is not shutting down in time. The system gives it a timeout to close gracefully, and Plasma needs more time than that timeout allows.\nunderstanding the problem When you reboot or shut down, systemd stops all running services. It sends a SIGTERM signal (like saying \u0026ldquo;please stop\u0026rdquo;) and waits for the service to exit. If the service does not exit within the timeout period, systemd kills it with SIGKILL (forceful kill).\nPlasma-plasmashell is the main KDE Plasma window manager. When it gets SIGTERM, it needs to clean up. Close open windows. Save state. Sync data. All of that takes time. On modern systems with lots of open applications, this can easily take more than 5 seconds.\nYour system was set to DefaultTimeoutStopSec=5s. That is the timeout for all system services. Five seconds is too short for Plasma. So Plasma gets killed mid-shutdown, and the system has to force a reboot.\nchecking your current state Look at your systemd config:\ngrep DefaultTimeoutStopSec /etc/systemd/system.conf If you see 5s or anything less than 10s, that is your problem.\nCheck the journal when it happens:\nsudo journalctl -b -1 | grep -i \u0026#34;timeout\\|plasma-plasmashell\u0026#34; Should show plasma-plasmashell timing out during shutdown.\nmy situation I had set DefaultTimeoutStopSec=5s to debug something else. Did not realize Plasma needs more time. Every reboot hung waiting for Plasma to exit. After 5 seconds, systemd gave up and killed it anyway.\nthe fix Increase the timeout to 30 seconds. That gives Plasma and other services plenty of time to shut down gracefully:\nsudo sed -i \u0026#39;s/DefaultTimeoutStopSec=5s/DefaultTimeoutStopSec=30s/\u0026#39; /etc/systemd/system.conf Verify it:\ngrep DefaultTimeoutStopSec /etc/systemd/system.conf Should show:\nDefaultTimeoutStopSec=30s Reload the systemd manager to apply the change:\nsudo systemctl daemon-reload Then reboot:\nsudo systemctl reboot Plasma will now shut down cleanly without hanging. It has 30 seconds instead of 5. That is more than enough time.\nwhy this works Plasma is not broken. It is not hanging. It is just a slow shutdown. KDE cleans up widgets, closes applications, saves session state. All of this takes time. Thirty seconds gives it breathing room to do all that without systemd killing it.\nThe default timeout on fresh Fedora installs is usually 90 seconds. I had manually lowered it to 5 seconds for troubleshooting. Forgot to raise it back. Plasma got caught in the middle.\nwhy the timeout exists Systemd has a timeout because otherwise a misbehaving service could hang forever and the system would never shut down. The timeout is there to prevent that. But it needs to be long enough for normal services to clean up properly. Thirty seconds is reasonable for most things, including Plasma.\ntesting it After rebooting, watch the shutdown sequence. You should not see any timeout messages for plasma-plasmashell. The system should reboot cleanly without hanging.\nCheck the next boot logs to confirm:\nsudo journalctl -b -1 | grep -i \u0026#34;plasma-plasmashell\\|timeout\u0026#34; Should not show timeout for Plasma. If it does, increase the timeout to 45 or 60 seconds.\nwhat not to do Do not set the timeout to 0 or disabled. That removes the protection against hung services. Leave it at 30 or higher, but not disabled.\nDo not kill Plasma manually during shutdown. The timeout mechanism is there for a reason. Let it shut down on its own.\nDo not ignore the problem hoping it will go away. Hanging on every reboot is annoying and can cause data loss if you hard reboot.\ntakeaway KDE Plasma takes more than 5 seconds to shut down gracefully. If your DefaultTimeoutStopSec is too low, Plasma gets killed before it finishes. Increase the timeout to 30 seconds. Plasma will exit cleanly and your reboots will work properly.\nThis applies to any heavy application. GUI applications, database servers, anything that needs time to clean up. If you see timeout messages for any service, increase the timeout and that service should behave properly.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/kde-plasma-hanging-on-shutdown-timeout/","summary":"\u003cp\u003eReboot the system. It hangs. Doesn\u0026rsquo;t crash, doesn\u0026rsquo;t give an error on screen, just sits there for what feels like forever. Eventually it times out and reboots anyway. But every single reboot does this.\u003c/p\u003e\n\u003cp\u003eCheck the logs and you see this:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eplasma-plasmashell.service: State \u0026#39;stop-sigterm\u0026#39; timed out. Aborting.\nplasma-plasmashell.service: Failed with result \u0026#39;timeout\u0026#39;.\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eKDE Plasma is not shutting down in time. The system gives it a timeout to close gracefully, and Plasma needs more time than that timeout allows.\u003c/p\u003e","title":"kde plasma hangs on reboot, plasma-plasmashell timeout error"},{"content":"I run a QNAP TS-459 Pro+ as my homelab NAS, connected to several Linux machines across different distros. Fedora 44, NixOS, Solus, whatever. The problem: I had to remember different commands to check disk status, restart NFS, fix permissions, manage keepalive scripts, diagnose remote access issues.\nInstead of hunting through notes or SSH-ing and fumbling around, I wanted one tool. A menu. Same commands, same approach, any distro. Works local or remote. I can call it from my home bin folder and it just works.\nthe why SSH into QNAP repeatedly is slow. Copy-pasting commands is error prone. Documentation spreads across five different places. Every time I boot a fresh distro install, I end up spending 30 minutes remembering how to mount shares or restart NFS.\nA menu-driven tool means I never have to remember the commands. Press 2, check status. Press 16, check disk SMART health. Press 18, restart keepalive. The tool handles the details.\nAlso, it detects network location automatically. If you are on the local network, it connects directly to 192.168.0.xxx. If you are remote, it uses the myqnapcloud hostname. No manual switching.\nthe tool The script does three main things:\nDetects if you are local or remote Shows a menu with 19 operations Runs the operation via SSH or direct commands Main operations:\nSSH access Status and health checks (memory, uptime, load) NFS management (list shares, restart service, fix permissions) Disk checks (usage, SMART data, load cycle counts) Remote access diagnosis (DNS resolution, port checks, connectivity tests) Setup tools (mount configuration, SSH keys, initial config) Optimization (memory cleaning, network buffer tweaks) Maintenance (keepalive process monitoring, disk head parking prevention) how to use it Get the script into your home bin folder:\nmkdir -p ~/bin curl https://raw.githubusercontent.com/tolgaerok/qnap-manager/main/qnap-manager -o ~/bin/qnap-manager chmod +x ~/bin/qnap-manager Add ~/bin to your PATH if it is not already there. In your bashrc:\nexport PATH=\u0026#34;$HOME/bin:$PATH\u0026#34; Then source it:\nsource ~/.bashrc Now just run:\nqnap-manager It shows a menu. Press the number for what you want.\ninside the script The script has a config section at the top:\nqnap_host=\u0026#34;jacksparrow.myxxxxd.com\u0026#34; qnap_user=\u0026#34;xxxx\u0026#34; qnap_ip=\u0026#34;192.168.0.xxx\u0026#34; Change these to match your QNAP. The IP gets masked in public posts but use your real local IP.\nThe check_network function pings your local IP. If it responds, you are local. If not, it switches to the remote hostname.\ncheck_network() { if ping -c 1 -W 1 $qnap_ip \u0026amp;\u0026gt;/dev/null; then echo \u0026#34;local\u0026#34; else echo \u0026#34;remote\u0026#34; fi } Every SSH operation calls this first. So everything automatically switches between local direct connection and remote access. No manual changes needed.\nkey operations explained status check Shows uptime, kernel version, load average, memory usage. Quick health snapshot:\n2) check qnap status disk smart health Pulls SMART data from all four disks. Shows runtime hours, load cycles, temperature, and critical values like reallocated sectors.\n16) check smart health This is useful because older drives (my disks 3 and 4 are about 7 years in) need monitoring. The script baseline compares against known values and shows you what changed.\nkeepalive status My WD Red drives on disks 3 and 4 have an aggressive head parking feature. Without regular disk access, they park every 8 minutes. That breaks NFS performance and kills the drives faster via constant parking cycles.\nKeepalive touches the disks every 30 minutes to prevent that. Option 17 checks if it is running, looks at the log, and compares current load cycle counts against baseline:\n17) disk keepalive status If keepalive stopped, option 18 restarts it and rewrites the autorun.sh script:\n18) fix keepalive setup tools Option 11 installs NFS, LFTP, SSH tools and configures mount points. Option 12 sets up passwordless SSH keys. Option 13 diagnoses why remote access might be failing.\nThese are one-time setup commands. Run them once on a fresh system and everything is configured.\nnetwork tweaks Option 19 applies buffer tuning to the QNAP kernel:\n19) fix network tweaks After firmware updates, these settings get reset. Run this to reapply them and persist to sysctl.conf.\nwhy this design Simple reasons:\nYou do not have to remember shell one-liners It works on any distro because it is pure bash It works on any machine because it SSH-es to QNAP Auto-detection of local vs remote removes manual switching All output goes through the same tool, same colors, same format One place to edit when QNAP IP changes or credentials update For me, it is faster to press 16 than to SSH in, run get_hd_smartinfo, pipe through grep, format the output. The tool does that every time consistently.\nthe approach I built this because troubleshooting always followed the same pattern:\nSSH to QNAP Run some command Check the output Maybe run a second command Check again Instead of documenting each step separately, I wrapped them into a menu. Each option is a small function that does one job well. The script runs SSH once and pipes all the commands together in the right order.\nThis is why I like menu-driven tools for homelab stuff. You end up using them more often because there is zero friction. Just run it, pick an option, get the answer.\nmasking and editing The script has your real IP and hostname at the top. Mask them for public posts:\nqnap_host=\u0026#34;hostname.myqxxxxx.com\u0026#34; qnap_user=\u0026#34;xxxx\u0026#34;\u0026#34; qnap_ip=\u0026#34;192.168.x.xxx\u0026#34; If you are sharing this publicly, mask the last octet. Keep the structure but hide the actual IP.\nFor your own use, fill in the real values and commit it to your private repo or keep it locally.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/qnap-manager-tool/","summary":"\u003cp\u003eI run a QNAP TS-459 Pro+ as my homelab NAS, connected to several Linux machines across different distros. Fedora 44, NixOS, Solus, whatever. The problem: I had to remember different commands to check disk status, restart NFS, fix permissions, manage keepalive scripts, diagnose remote access issues.\u003c/p\u003e\n\u003cp\u003eInstead of hunting through notes or SSH-ing and fumbling around, I wanted one tool. A menu. Same commands, same approach, any distro. Works local or remote. I can call it from my home bin folder and it just works.\u003c/p\u003e","title":"qnap manager : one menu driven tool for all my qnap operations"},{"content":"G4 desktop, Fedora 44 on SDA. Decided to try RakuOS on a spare NVMe. Booted the G4 again and Fedora wouldn\u0026rsquo;t boot from SDA.\nThe EFI entry exists. The files are there. But the UEFI firmware just skips SDA and boots something else.\nunderstanding the problem Your computer\u0026rsquo;s EFI system (UEFI firmware) has a boot order list. That list points to files on disk that tell the firmware how to boot each OS. If that pointer gets broken or the bootloader itself isn\u0026rsquo;t properly initialized, the entry becomes dead weight. It exists but doesn\u0026rsquo;t work.\nWhen you install another distro on a different drive, the UEFI firmware can lose track of what it\u0026rsquo;s supposed to do with Secure Boot settings. It gets paranoid. Even if you disabled Secure Boot in BIOS, the firmware cached the old state or just doesn\u0026rsquo;t trust you anymore.\nthe standard fix (what works 90% of the time) First, regenerate the GRUB configuration file. This tells GRUB what operating systems are available and how to boot them:\nsudo grub2-mkconfig -o /boot/grub2/grub.cfg What this does: GRUB scans your disks, finds all installed OSes, and writes them into a config file. It found my CachyOS on NVMe and Fedora on SDA.\nOutput on my system:\nGenerating grub configuration file ... Found CachyOS on /dev/nvme0n1p2 Adding boot menu entry for UEFI Firmware Settings ... done That worked fine. Then I tried to actually install the bootloader to the EFI partition:\nsudo grub2-install --efi-directory=/boot/efi /dev/sda What this does: grub2-install writes the actual bootloader code to your EFI System Partition on /dev/sda. This is what the firmware actually reads when it boots. It\u0026rsquo;s like writing the actual boot instructions to disk.\nBut it threw this at me:\nInstalling for x86_64-efi platform. grub2-install: error: This utility should not be used for EFI platforms because it does not support UEFI Secure Boot. Make sure Secure Boot is disabled before proceeding. Translation: \u0026ldquo;Hey, I think Secure Boot is on, and I won\u0026rsquo;t write the bootloader if it is, because Secure Boot will just refuse to load it anyway.\u0026rdquo;\nchecking if secure boot is actually off This is the part where you verify. BIOS says Secure Boot is off, but grub2-install doesn\u0026rsquo;t believe it. Check what the system actually thinks:\nmokutil --sb-state What this does: mokutil is the \u0026ldquo;Machine Owner Key\u0026rdquo; utility. It talks directly to the firmware and tells you the actual Secure Boot state, not what BIOS settings say. This is the source of truth.\nOutput on my system:\nSecureBoot disabled 54 52 10 Secure Boot is literally disabled. The firmware confirmed it. But grub2-install still refused.\nchecking the boot entries Let\u0026rsquo;s see what the firmware actually has registered:\nefibootmgr -v What this does: efibootmgr lists every boot entry the firmware knows about. It shows the order they boot in, what they point to, and whether they\u0026rsquo;re active. The -v flag shows verbose details.\nMy output (simplified):\nBootOrder: 0006,0000,0004,0003 Boot0006* Fedora 44 HD(1,GPT,e7578f70-a442-4cdb-9c71-83899fcd2b5e,0x800,0x12c000)/\\EFI\\fedora\\shimx64.efi Boot0001* Fedora HD(1,GPT,e7578f70-a442-4cdb-9c71-83899fcd2b5e,0x800,0x12c000)/\\EFI\\fedora\\shimx64.efi Translation: Boot entry 0006 is first in the boot order. Both 0006 and 0001 point to the same EFI file (shimx64.efi) on the same partition. Everything looks right on paper. The firmware should follow that pointer.\nBut it doesn\u0026rsquo;t. Why? Because the actual GRUB bootloader code in the EFI System Partition is out of sync with the boot entry. The entry points to the file, but the file isn\u0026rsquo;t properly initialized. The firmware looks at the entry, tries to follow it, finds nothing it recognizes, and gives up.\nthe actual fix This is the part that works:\nsudo grub2-install --efi-directory=/boot/efi --force /dev/sda The --force flag is the key. It tells grub2-install: \u0026ldquo;Stop complaining about Secure Boot. I verified it\u0026rsquo;s off with mokutil. Write the bootloader anyway.\u0026rdquo;\nWhat this actually does: grub2-install writes the bootloader code directly to the EFI System Partition. It initializes the firmware\u0026rsquo;s EFI environment so that when the boot entry points to shimx64.efi, the firmware can actually find it and boot it.\nOutput on my system:\nInstalling for x86_64-efi platform. Done. No errors. That\u0026rsquo;s it. No complaints. The bootloader is now written.\nwhy this works The problem was never the boot entry itself. The entry existed and pointed to the right place. The problem was that the bootloader code wasn\u0026rsquo;t actually initialized in the EFI partition. The entry was a pointer to nothing.\nWhen you installed RakuOS on a different drive, the UEFI firmware\u0026rsquo;s internal state got confused. Maybe it cached old Secure Boot settings, maybe it just got paranoid. Either way, grub2-install didn\u0026rsquo;t trust that Secure Boot was actually off.\nBut you verified it with mokutil. The firmware confirmed it. So using --force is safe. It just writes what should have been there all along.\nthe full steps i actually ran Regenerate GRUB config: sudo grub2-mkconfig -o /boot/grub2/grub.cfg Verify Secure Boot is off: mokutil --sb-state # SecureBoot disabled Check the boot entries (optional but useful): efibootmgr -v # Both Fedora entries pointing to the right EFI file! Force grub2-install to write the bootloader: sudo grub2-install --efi-directory=/boot/efi --force /dev/sda Reboot: sudo systemctl reboot Fedora 44 on SDA booted. Back in business.\ntakeaway Multibooting or testing other distros on spare drives can leave the UEFI firmware in a confused state. grub2-install gets paranoid about Secure Boot even when it\u0026rsquo;s actually off.\nAlways verify actual state with mokutil before wasting time. If grub2-install still refuses despite mokutil confirming Secure Boot is disabled, use --force. It\u0026rsquo;s safe. The bootloader just needs permission to initialize itself.\nDon\u0026rsquo;t re-disable Secure Boot. Don\u0026rsquo;t reinstall packages in different versions. Just force it and move on.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/efi-boot-entry-recovery/","summary":"\u003cp\u003eG4 desktop, Fedora 44 on SDA. Decided to try RakuOS on a spare NVMe. Booted the G4 again and Fedora wouldn\u0026rsquo;t boot from SDA.\u003c/p\u003e\n\u003cp\u003eThe EFI entry exists. The files are there. But the UEFI firmware just skips SDA and boots something else.\u003c/p\u003e\n\u003ch2 id=\"understanding-the-problem\"\u003eunderstanding the problem\u003c/h2\u003e\n\u003cp\u003eYour computer\u0026rsquo;s EFI system (UEFI firmware) has a boot order list. That list points to files on disk that tell the firmware how to boot each OS. If that pointer gets broken or the bootloader itself isn\u0026rsquo;t properly initialized, the entry becomes dead weight. It exists but doesn\u0026rsquo;t work.\u003c/p\u003e","title":"lost efi boot entry on fedora 44 after raku os, grub2-install won't cooperate"},{"content":"Why I wanted this backed up properly This whole site, every post, the theme, the config, lives in one folder on the T620. Nothing about that folder is protected by anything beyond the disk it sits on. Wanted a real backup, and wanted it copied off the box entirely, not just sitting in a second folder on the same machine.\nFirst attempt at excluding the build output Wanted to back up the Hugo source without dragging along the generated public folder or the build cache, since both regenerate automatically from a normal deploy and just bloat the archive for no reason:\nsudo tar -czvf /root/hugo-kingtolga-backup-$(date +%Y%m%d).tar.gz -C /home/tolga/hugo kingtolga --exclude=\u0026#39;kingtolga/public\u0026#39; --exclude=\u0026#39;kingtolga/resources\u0026#39; Ran it. Watched the output scroll past. Public folder was right there in the listing anyway, exclude flags did nothing. Tar spat out the real reason at the end:\ntar: The following options were used after non-option arguments. These options are positional and affect only arguments that follow them. tar: --exclude has no effect GNU tar treats exclude flags as positional, meaning anything placed after the actual file arguments in the command gets silently ignored. Moved them to the front instead:\nsudo tar --exclude=\u0026#39;kingtolga/public\u0026#39; --exclude=\u0026#39;kingtolga/resources\u0026#39; --exclude=\u0026#39;kingtolga/themes/PaperMod/.git\u0026#39; -czvf /root/hugo-kingtolga-backup-$(date +%Y%m%d).tar.gz -C /home/tolga/hugo kingtolga Threw in excluding the theme\u0026rsquo;s own git history too while I was at it, no reason to carry that along either since it can just be re-cloned if ever needed.\nChecking it worked, and hitting the same shell quirk again ls -la /root/hugo-kingtolga-backup-*.tar.gz ls: cannot access \u0026#39;/root/hugo-kingtolga-backup-*.tar.gz\u0026#39;: Permission denied Same thing that\u0026rsquo;s bitten me before on this exact box. My regular user can\u0026rsquo;t read into root\u0026rsquo;s home directory, so the wildcard never actually expands, bash just passes the literal asterisk through and the command fails before sudo even gets a chance to elevate. Wrapping the whole thing in a subshell fixes it:\nsudo bash -c \u0026#39;ls -la /root/hugo-kingtolga-backup-*.tar.gz\u0026#39; That worked, real file, real size, confirmed.\nGetting it off the box, attempt one, wrong IP entirely Wanted a copy on my other Fedora desktop, not just sitting on the same machine that could theoretically die and take the only copy with it. Tried scp to an IP I had written down from memory:\nsudo bash -c \u0026#39;scp /root/hugo-kingtolga-backup-20260720.tar.gz tolga@192.168.0.XX1:/home/tolga/\u0026#39; ssh: connect to host 192.168.0.XX1 port 22: No route to host Pinged it directly to rule out a firewall issue first:\nping -c 3 192.168.0.XX1 Destination Host Unreachable Genuinely the wrong address, not a config problem. The IP I had in my head for that machine was stale, left over from something else entirely. Found the actual current address and tried again.\nAttempt two, right IP, wrong service state sudo bash -c \u0026#39;scp /root/hugo-kingtolga-backup-20260720.tar.gz tolga@192.168.0.XX2:/home/tolga/\u0026#39; ssh: connect to host 192.168.0.XX2 port 22: Connection refused Different error this time, and a meaningfully different one. No route to host means the machine can\u0026rsquo;t be reached at all. Connection refused means the machine is reachable but actively rejecting the connection, which usually means the service just isn\u0026rsquo;t running. Confirmed with a ping first:\nping -c 3 192.168.0.XX2 Real replies, real round trip times, machine was genuinely up. So the problem had to be sshd itself. Checked on that machine directly:\nsudo systemctl status sshd Loaded: loaded (...disabled; preset: disabled) Active: inactive (dead) Just never enabled. Fixed it:\nsudo systemctl enable --now sshd Attempt three, service running, transfer still failing sudo bash -c \u0026#39;scp /root/hugo-kingtolga-backup-20260720.tar.gz tolga@192.168.0.XX2:/home/tolga/\u0026#39; Got past the connection this time, prompted for a password, then:\nscp: Received message too long 168427520 scp: Ensure the remote shell produces no output for non-interactive sessions. Recognised this one. My bashrc on every machine I run has a proper banner, a fortune quote piped through lolcat, an uptime summary, the works, all firing automatically at the bottom of the file on every shell startup. scp opens a brief, non-interactive shell on the remote end to actually perform the copy, and that shell was firing the entire banner sequence straight into the data stream scp expects to be silent, corrupting the transfer before a single real byte got through.\nThe actual fix Added an early return right after the bashrc header, before anything else in the file gets a chance to run, so a non-interactive shell exits immediately instead of executing the banner and fortune output:\ncase $- in *i*) ;; *) return;; esac That checks the shell\u0026rsquo;s own option flags for the letter i, which is only present on a genuinely interactive session. Anything else, including the quick shell scp opens for the file copy, hits return immediately and stays completely silent from that point on.\nThe actual transfer, finally sudo bash -c \u0026#39;scp /root/hugo-kingtolga-backup-20260720.tar.gz /root/www-kingtolga-backup-20260720.tar.gz tolga@192.168.0.XX2:/home/tolga/\u0026#39; Clean transfer, real progress bars, both files landed. Confirmed on the other end:\nssh tolga@192.168.0.XX2 \u0026#39;ls -la /home/tolga/hugo-kingtolga-backup-20260720.tar.gz /home/tolga/www-kingtolga-backup-20260720.tar.gz\u0026#39; Making it automatic going forward Folded the hugo folder into the existing weekly config backup cron job so this happens without me remembering to run it by hand:\nsudo sed -i \u0026#39;s|/root/backup-staging /etc/ssh /etc/samba /usr/local/bin|/root/backup-staging /etc/ssh /etc/samba /usr/local/bin /home/tolga/hugo|\u0026#39; /etc/cron.d/config-backup What actually went wrong, start to finish Exclude flags in the wrong position in the tar command, the same sudo-versus-shell-glob timing issue I have hit more than once on this box, a genuinely stale IP address written down from memory instead of checked, an SSH service that was simply never enabled on the target machine, and a bashrc firing full interactive output into a protocol that needs total silence. None of it was one clean fix. Every step taught me something the previous one didn\u0026rsquo;t, and the actual working sequence only exists because each wrong attempt narrowed down exactly what was left to check next.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/hugo-blog-backup-saga/","summary":"\u003ch2 id=\"why-i-wanted-this-backed-up-properly\"\u003eWhy I wanted this backed up properly\u003c/h2\u003e\n\u003cp\u003eThis whole site, every post, the theme, the config, lives in one folder\non the T620. Nothing about that folder is protected by anything beyond\nthe disk it sits on. Wanted a real backup, and wanted it copied off the\nbox entirely, not just sitting in a second folder on the same machine.\u003c/p\u003e\n\u003ch2 id=\"first-attempt-at-excluding-the-build-output\"\u003eFirst attempt at excluding the build output\u003c/h2\u003e\n\u003cp\u003eWanted to back up the Hugo source without dragging along the generated\npublic folder or the build cache, since both regenerate automatically\nfrom a normal deploy and just bloat the archive for no reason:\u003c/p\u003e","title":"Backing Up This Blog: Every Wrong Turn Before It Actually Worked"},{"content":"The symptom Added client-side search to this site using PaperMod\u0026rsquo;s built-in feature. Worked immediately, which should have been my first clue something was off, because it worked badly. Searching \u0026ldquo;kyber,\u0026rdquo; the full, exact word, appearing in exactly one post, returned close to a dozen completely unrelated results. Searching just \u0026ldquo;ky\u0026rdquo; returned the one correct result and nothing else. Backwards from how search should behave, more specific input giving worse results than less specific input.\nFirst attempt, which did nothing Set this in hugo.toml, assuming it was the actual control for fuzziness:\n[params] fuzzysearch = true Tested it. Broken results. Flipped it to false:\n[params] fuzzysearch = false Rebuilt, redeployed. Zero change in behaviour either way, \u0026ldquo;kyber\u0026rdquo; still returned the same pile of unrelated posts.\nSecond attempt, assuming it was a caching problem At this point I figured Hugo was serving a stale search index from somewhere, since a config change producing literally no difference usually means old output is still being served. Did a full clean rebuild instead of the normal deploy script:\ncd ~/hugo/kingtolga rm -rf public resources hugo --minify --cleanDestinationDir sudo /home/tolga/hugo-deploy.sh Also tried running hugo-deploy as the shorter installed command and hit a wall I\u0026rsquo;d forgotten about:\nsudo hugo-deploy sudo: hugo-deploy: command not found Turned out the move into /usr/local/bin from earlier never actually happened on this box, so I fell back to the full path. Redeployed with the clean build. Still broken. Same results, exact word search still pulling in unrelated posts. At this point the setting itself was the actual problem, not caching, and I\u0026rsquo;d wasted a rebuild cycle chasing the wrong theory.\nActually finding the real mechanism Stopped guessing at config values entirely and went into the theme\u0026rsquo;s own source to see what it was actually reading:\ngrep -r \u0026#34;fuzzysearch\\|fuse\\|lunr\u0026#34; ~/hugo/kingtolga/themes/PaperMod/layouts/_partials/*.html ~/hugo/kingtolga/themes/PaperMod/assets/js/*.js That surfaced it. PaperMod\u0026rsquo;s search runs on Fuse.js, and the actual options object it reads is site.Params.fuseOpts, a nested table with real Fuse.js configuration keys. fuzzysearch was never a parameter this theme checks for at all. I\u0026rsquo;d invented a setting that happened to parse as valid TOML and did precisely nothing, twice, in two different values, across two rebuilds.\nThe real fix [params.fuseOpts] isCaseSensitive = false shouldSort = true location = 0 distance = 0 threshold = 0.0 minMatchCharLength = 3 keys = [\u0026#34;title\u0026#34;, \u0026#34;permalink\u0026#34;, \u0026#34;summary\u0026#34;, \u0026#34;content\u0026#34;] Removed the fake fuzzysearch line entirely first:\nsed -i \u0026#39;/^ fuzzysearch = false/d\u0026#39; ~/hugo/kingtolga/hugo.toml threshold is the setting that actually mattered. Fuse.js scores matches on a 0 to 1 scale, 0.0 means only an exact match counts, 1.0 matches almost anything. Left unset, it defaults to roughly 0.4, which explains exactly what I was seeing the whole time: loose, low confidence matches scattered across posts that never actually contained the search term.\ndistance = 0 tightens it further by removing tolerance for how far a matched substring can sit from where Fuse.js expects it in the text. minMatchCharLength = 3 stops it from trying to match on one or two characters at all, which is why \u0026ldquo;ky\u0026rdquo; alone was returning a real result the whole time, that result was legitimate, just riding alongside a threshold loose enough to also return everything else.\nApplying it, correctly this time cd ~/hugo/kingtolga rm -rf public resources sudo /home/tolga/hugo-deploy.sh Same clean rebuild step I\u0026rsquo;d already tried once for the wrong reason, this time actually paired with a real config change. Hard refreshed the search page in the browser to rule out the browser itself caching the old Fuse.js index, then tested \u0026ldquo;kyber\u0026rdquo; and \u0026ldquo;ky\u0026rdquo; again. Fixed, finally.\nWhat actually fixed it, and what wasted time first fuzzysearch, invented, did nothing, tested in both true and false, cost two full rebuild cycles chasing a setting that was never real. The clean rebuild itself was the right instinct but applied at the wrong moment, it fixed nothing because the underlying config was still wrong, not stale. threshold, distance, and minMatchCharLength under the real fuseOpts table were the only things that changed the actual behaviour. Worth remembering next time a config value seems to do nothing at all, that is the signal to go check the theme\u0026rsquo;s own source immediately, rather than re-testing the same wrong parameter a second time first.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/hugo-search-fix/","summary":"\u003ch2 id=\"the-symptom\"\u003eThe symptom\u003c/h2\u003e\n\u003cp\u003eAdded client-side search to this site using PaperMod\u0026rsquo;s built-in\nfeature. Worked immediately, which should have been my first clue\nsomething was off, because it worked badly. Searching \u0026ldquo;kyber,\u0026rdquo; the\nfull, exact word, appearing in exactly one post, returned close to a\ndozen completely unrelated results. Searching just \u0026ldquo;ky\u0026rdquo; returned the\none correct result and nothing else. Backwards from how search should\nbehave, more specific input giving worse results than less specific\ninput.\u003c/p\u003e","title":"Fixing Hugo's Search: fuzzysearch Was Never a Real Setting"},{"content":"Fedora 44, KDE Plasma, Wayland session. Simple thing, holding a key down.\n7777777777777777777 Except it wasn\u0026rsquo;t doing that anymore. Hold 7, get one 7, and instead of it repeating I\u0026rsquo;d get a little popup underneath the cursor with a handful of characters sitting in it. Looked exactly like one of those mobile long-press accent menus.\nChecked System Settings → Input Devices → Keyboard → Key repeat. All fine, delay and rate both set correctly. Tried the Test area right there in Settings, same thing happened, single character then that popup. Tried it in a terminal, tried it in the browser, tried it in an editor. Everywhere. Not one app misbehaving, the whole session.\nFirst guess was a Wayland/KWin session-level bug. Wrong guess.\nwhat it actually was That popup wasn\u0026rsquo;t an accent picker. It was IBus\u0026rsquo;s candidate window.\nSomewhere along the line, IBus Wayland had ended up set as the active virtual keyboard under Plasma. The second IBus is sitting in that role on Wayland, it grabs the held key as an input-method event instead of letting KWin pass it straight through for normal repeat. Instead of the key repeating, you get an IBus candidate popup, which looks exactly like a dead keyboard if you don\u0026rsquo;t already know what you\u0026rsquo;re staring at.\nNot a one-off either. Same thing bites other people on Plasma 6 Wayland, arrows, enter, backspace, whatever, the moment IBus Wayland is the active virtual keyboard.\nthe fix System Settings → Input Devices → Virtual Keyboard\nSet to IBus Wayland → switch it to None, or grab Fcitx5 and switch to that instead. Fcitx5 doesn\u0026rsquo;t carry this bug. Newer Plasma builds also ship a native Plasma Keyboard option now, pick that if it\u0026rsquo;s there, sidesteps the whole mess.\nCheck it\u0026rsquo;s actually gone afterward:\nsystemctl --user status ibus-daemon Still running and you don\u0026rsquo;t need it, no CJK or complex input methods in play, kill it:\nsystemctl --user disable --now ibus-daemon Worth a check too whether it snuck in as a dependency of something else entirely:\ndnf list installed | grep ibus takeaway Nothing wrong with the GPU driver, KWin, or the repeat settings themselves, this was an input method grabbing the key event before repeat ever got a look in. Fedora KDE, Wayland, repeat suddenly dies with a weird little popup standing in for it, check Virtual Keyboard before chasing anything else.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/kde-wayland-key-repeat-ibus/","summary":"\u003cp\u003eFedora 44, KDE Plasma, Wayland session. Simple thing, holding a key down.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e7777777777777777777\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eExcept it wasn\u0026rsquo;t doing that anymore. Hold \u003ccode\u003e7\u003c/code\u003e, get one \u003ccode\u003e7\u003c/code\u003e, and instead of it repeating I\u0026rsquo;d get a little popup underneath the cursor with a handful of characters sitting in it. Looked exactly like one of those mobile long-press accent menus.\u003c/p\u003e\n\u003cp\u003eChecked System Settings → Input Devices → Keyboard → Key repeat. All fine, delay and rate both set correctly. Tried the Test area right there in Settings, same thing happened, single character then that popup. Tried it in a terminal, tried it in the browser, tried it in an editor. Everywhere. Not one app misbehaving, the whole session.\u003c/p\u003e","title":"keys not repeating on fedora kde wayland, blame ibus"},{"content":"Brian\u0026rsquo;s running Ultramarine, which is Fedora underneath, and wanted SELinux disabled outright. Fair enough, plenty of people go straight for that.\nthe disable route Two ways to actually do it. The config file way:\nsudo sed -i \u0026#39;s/^SELINUX=.*/SELINUX=disabled/\u0026#39; /etc/selinux/config sudo reboot Or through grubby instead, setting it as a kernel argument, which survives even if the config file gets reverted or overwritten somehow:\nsudo grubby --update-kernel=ALL --args=\u0026#34;selinux=0\u0026#34; sudo reboot Check it landed after reboot:\ngetenforce Should say Disabled.\npermissive first Note: fully disabling SELinux means no policy loads at all, and turning it back on later is a proper pain. You need a full relabel, touch /.autorelabel then reboot, and on a bigger drive that can sit there for ten plus minutes looking like it\u0026rsquo;s hung when it\u0026rsquo;s actually just working through every file on disk. His box is a fresh Ultramarine install, nothing complicated running on it, no services to worry about breaking. Still made the case for permissive first anyway, just as a general habit. It logs every denial it would have blocked instead of actually blocking anything, so you get to see what SELinux was about to stop before you commit to switching it off completely. Costs nothing to check first.\nre-enabling it later if later brian wanting it back on. The command\u0026rsquo;s simple enough:\nsudo sed -i \u0026#39;s/^SELINUX=.*/SELINUX=enforcing/\u0026#39; /etc/selinux/config sudo touch /.autorelabel sudo reboot That /.autorelabel step isn\u0026rsquo;t optional. Skip it and every file\u0026rsquo;s security context is wrong, since none of that labeling happened while SELinux was off. Do expect the reboot to look stuck for a while, that\u0026rsquo;s normal, it\u0026rsquo;s relabeling the whole filesystem in the background. Checked back in and getenforce came back Permissive, so somewhere in the conversation he\u0026rsquo;d actually landed on permissive instead of full enforcing. Fine outcome either way, better than disabled. Ran the full toggle test to make sure both directions actually worked before deciding what to leave it on:\nsudo setenforce 1 getenforce Flipped clean to Enforcing.\nthen i couldn\u0026rsquo;t figure out his hostname Terminal was Konsole, Dolphin sitting open behind it, and the hostname staring back at me was just 2001-1960-600c-0d6a-7fba-72a2-712c-05b9. Took a second to realise that\u0026rsquo;s literally his IPv6 address with the colons swapped for dashes. Classic Tailscale MagicDNS behaviour when a device never gets a proper hostname set, it just falls back to using the address itself as the name. Sorted that for him too:\nsudo hostnamectl set-hostname brians-penis New terminal sessions should show brian@brians-penis from here on instead of a wall of hex. Small job, ended up covering disable versus permissive versus enforcing, the autorelabel gotcha, and a hostname that looked like a ransom note. All in one sitting.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/selinux-disabled-permissive-brians-box/","summary":"\u003cp\u003eBrian\u0026rsquo;s running Ultramarine, which is Fedora underneath, and wanted SELinux disabled outright. Fair enough, plenty of people go straight for that.\u003c/p\u003e\n\u003ch2 id=\"the-disable-route\"\u003ethe disable route\u003c/h2\u003e\n\u003cp\u003eTwo ways to actually do it. The config file way:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esudo sed -i \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;s/^SELINUX=.*/SELINUX=disabled/\u0026#39;\u003c/span\u003e /etc/selinux/config\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esudo reboot\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOr through grubby instead, setting it as a kernel argument, which survives even if the config file gets reverted or overwritten somehow:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esudo grubby --update-kernel\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003eALL --args\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;selinux=0\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esudo reboot\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCheck it landed after reboot:\u003c/p\u003e","title":"Disabled, Permissive, or Enforcing, Sorting Out SELinux on Brian's Box"},{"content":"Simple job. Copy Videos folder from G4 over to JackSparrow2 with rsync. Done this a hundred times.\nrsync -avP Videos/ tolga@100.xxx.xxx.xxx:/home/tolga/rsync/ protocol version mismatch, is your shell clean? (see the rsync manpage for an explanation) rsync error: protocol incompatibility (code 2) at compat.c(625) [sender=3.4.4] Never seen that one before. Password prompt worked fine, so the connection itself was good. Something after that was the problem.\nfirst thing i found SSH\u0026rsquo;d in normally to have a look and there it was, plain as day. Every time I log into JackSparrow2 I get a full ASCII banner, box drawing characters, the works. Looks great for a normal login. But rsync uses SSH under the hood too, to spawn a remote process, and it does not want anything on that connection except rsync\u0026rsquo;s own protocol data. Any stray text before the handshake and it falls over exactly like this. So the banner was my first suspect. Went looking in /etc/motd, that\u0026rsquo;s the Cockpit and Jellyfin links box, not what was printing. Checked /etc/motd.d/, just a Cockpit symlink, nothing there either. Grepped for the actual banner text and found it in /etc/ssh/banner, being force fed on every single SSH connection through a Banner directive sitting in /etc/ssh/sshd_config.d/99-jacksparrow2.conf. That directive does not care whether the session is interactive or not, it just fires. Commented it out, restarted sshd. Ran the rsync again.\nprotocol version mismatch, is your shell clean? Same error.\nsecond thing i found Went back into that same config file and PrintMotd yes was still sitting there, live, right above the Banner line I\u0026rsquo;d just killed. Commented that one out too, another sshd restart. Ran the rsync a third time. Still the exact same error. At this point I was starting to think it was something bigger, maybe a genuine rsync version mismatch between the two boxes, maybe something in the SSH multiplexing config. Checked both, nothing there. Went back to basics and just tried a plain SSH command instead of rsync, something with no banner logic attached at all.\nssh tolga@100.xxx.xxx.xxx \u0026#34;echo test\u0026#34; If you want to program in C, program in C. It\u0026#39;s a nice language. I use it occasionally... :-) -- Larry Wall in \u0026lt;7577@jpl-devvax.JPL.NASA.GOV\u0026gt; test There it was. A fortune cookie quote. Not the ASCII banner this time, a completely different thing, coming from somewhere else entirely, and it was still firing even with both sshd fixes in place. That confirmed the sshd side was actually clean now, this was something else altogether.\nthe actual culprit grep -rn \u0026#34;fortune\u0026#34; ~/.bashrc /home/tolga/.bashrc:27:echo \u0026#34; \u0026#34; \u0026amp;\u0026amp; fortune | lolcat \u0026amp;\u0026amp; echo \u0026#34; \u0026#34; Line 27 of my own .bashrc. A fortune quote piped through lolcat, unconditionally, on every single shell that spins up, including the tiny non-interactive one rsync creates behind the scenes to run its remote process. Three completely separate things had been printing banners this whole time. SSH\u0026rsquo;s own Banner directive, PrintMotd, and now a fortune cookie sitting in my bashrc, all doing the same job of corrupting the handshake, from three different places. Wrapped the whole thing in an interactive shell check so it only fires for real logins:\ncase $- in *i*) ;; *) return ;; esac That guard goes at the very top of .bashrc, before anything decorative runs. If the shell isn\u0026rsquo;t interactive, bash just returns straight out of the file before it ever reaches the fortune line.\nand then rsync -avP ./ tolga@100.xxx.xxx.xxx:/home/tolga/rsync/ sending incremental file list ./ Actions Will Always Reveal The Truth.webm 913,508 100% 279.98MB/s 0:00:00 (xfr#1, to-chk=21/23) ... You Never Owned It Anyway.webm 865,846 100% 781.47kB/s 0:00:01 (xfr#22, to-chk=0/23) sent 23,844,147 bytes received 437 bytes 1,445,126.30 bytes/sec total size is 23,836,327 speedup is 1.00 Yes. Finally. Clean transfer, full speed, every file landed. Ran it a second time straight after just to check the incremental behaviour was working properly too, and it came back near instant since nothing had actually changed.\nsent 1,151 bytes received 12 bytes 258.44 bytes/sec total size is 23,836,327 speedup is 20,495.55 Exactly what rsync\u0026rsquo;s supposed to do on a repeat run. Also learned something dumb along the way, the first rsync attempt after the fix failed for a different reason entirely, wrong path, because I was already sitting inside the Videos folder and told rsync to sync Videos/ again, which went looking for Videos inside Videos. Fixed that one by just pointing rsync at ./ instead since I was already standing in the right place. Three banners, one folder mixup, and one working transfer. If rsync or scp ever throws \u0026ldquo;protocol version mismatch, is your shell clean\u0026rdquo; at you, it means exactly what it says. Something is printing to the stream before the protocol handshake gets a chance to run. Check sshd\u0026rsquo;s Banner directive, check PrintMotd, and check your own shell rc files for anything that prints unconditionally, cowsay, fortune, neofetch, custom ASCII art, all of it. Any of them will do this. Mine had all three stacked on top of each other.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/protocol-version-mismatch-is-your-shell-clean/","summary":"\u003cp\u003eSimple job. Copy Videos folder from G4 over to JackSparrow2 with rsync. Done this a hundred times.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ersync -avP Videos/ tolga@100.xxx.xxx.xxx:/home/tolga/rsync/\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eprotocol version mismatch, is your shell clean?\n(see the rsync manpage for an explanation)\nrsync error: protocol incompatibility (code 2) at compat.c(625) [sender=3.4.4]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eNever seen that one before. Password prompt worked fine, so the connection itself was good. Something after that was the problem.\u003c/p\u003e\n\u003ch2 id=\"first-thing-i-found\"\u003efirst thing i found\u003c/h2\u003e\n\u003cp\u003eSSH\u0026rsquo;d in normally to have a look and there it was, plain as day. Every time I log into JackSparrow2 I get a full ASCII banner, box drawing characters, the works. Looks great for a normal login. But rsync uses SSH under the hood too, to spawn a remote process, and it does not want anything on that connection except rsync\u0026rsquo;s own protocol data. Any stray text before the handshake and it falls over exactly like this.\nSo the banner was my first suspect. Went looking in \u003ccode\u003e/etc/motd\u003c/code\u003e, that\u0026rsquo;s the Cockpit and Jellyfin links box, not what was printing. Checked \u003ccode\u003e/etc/motd.d/\u003c/code\u003e, just a Cockpit symlink, nothing there either. Grepped for the actual banner text and found it in \u003ccode\u003e/etc/ssh/banner\u003c/code\u003e, being force fed on every single SSH connection through a \u003ccode\u003eBanner\u003c/code\u003e directive sitting in \u003ccode\u003e/etc/ssh/sshd_config.d/99-jacksparrow2.conf\u003c/code\u003e. That directive does not care whether the session is interactive or not, it just fires. Commented it out, restarted sshd.\nRan the rsync again.\u003c/p\u003e","title":"protocol version mismatch, is your shell clean"},{"content":"Started with something dumb. Facebook wouldn\u0026rsquo;t load in Vivaldi or Firefox on the desktop. Worked fine on mobile. Figured it was a browser thing at first. Extension, cookies, the usual 💨 . Wasn\u0026rsquo;t that.\nwhy i thought i might need ipv6 Ran a curl test forcing each protocol separately, just to rule stuff in or out:\ncurl -4 -I https://www.facebook.com curl -6 -I https://www.facebook.com IPv4 came back clean. HTTP/2 200, instant. IPv6 flat out refused to connect:\ncurl: (7) Failed to connect to www.facebook.com port 443 after 13 ms: Could not connect to server 13ms is not a timeout. That\u0026rsquo;s an instant reject. So my first thought was okay, maybe my browser\u0026rsquo;s trying IPv6 first, which is normal, \u0026ldquo;happy eyeballs\u0026rdquo; 👀 is supposed to fall back to v4 cleanly, and something about the v6 path on my end is broken badly enough that the fallback isn\u0026rsquo;t happening smooth. That felt like something worth fixing properly rather than papering over, since I run Tailscale, which does use its own IPv6 range for the mesh. So I didn\u0026rsquo;t want to just nuke IPv6 blind and break something I actually rely on.\nwhat i actually found Checked what IPv6 addresses I even had:\nip -6 addr show ip -6 route show inet6 fe80::10cf:d378:c811:22e7/64 scope link link-local, not routable anywhere inet6 fd7a:115c:a1e0::5937:ae6e/128 dev tailscale0 Tailscale\u0026#39;s own private range That\u0026rsquo;s it. No global IPv6 address. No default route out to the actual internet over v6. Confirmed it with:\nping6 -c 3 2a03:2880:f384:1:face:b00c:0:25de ping6: connect: Network is unreachable Not \u0026ldquo;unreachable\u0026rdquo; like a firewall\u0026rsquo;s blocking it. Unreachable like there\u0026rsquo;s genuinely no route to send the packet on in the first place. My ISP or router just isn\u0026rsquo;t handing out real IPv6 at all. Never was. So none of this was ever a \u0026ldquo;fix the routing\u0026rdquo; job. There was nothing broken to fix. I simply never had IPv6 internet access, only Tailscale\u0026rsquo;s private overlay range and useless link-local addressing that can\u0026rsquo;t go anywhere off this machine.\nwhy i disabled it anyway Once I knew that, keeping IPv6 \u0026ldquo;enabled\u0026rdquo; system-wide was just actively costing me time on every single connection. Every app that tries IPv6 first has to fail, wait, then fall back to IPv4. That\u0026rsquo;s exactly the 13ms then fallback pattern that was making Facebook, and probably other sites, slower or flakier to load than they needed to be.\nTailscale doesn\u0026rsquo;t care either way. It manages its own IPv6 range internally regardless of what the OS does with public-facing IPv6, so turning off the system\u0026rsquo;s attempt at real-world IPv6 doesn\u0026rsquo;t touch the mesh at all.\nSo, kill it at the sysctl level, system-wide:\nsudo nano /etc/sysctl.d/99-disable-ipv6.conf net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 net.ipv6.conf.lo.disable_ipv6 = 1 Apply without a reboot:\nsudo sysctl --system Done. Every app on the box now goes straight to IPv4, first try, no dead end detour, no fallback delay.\nturns out it was never ipv6 anyway Here\u0026rsquo;s the kicker. After all that, Facebook still wouldn\u0026rsquo;t load. Turned out it wasn\u0026rsquo;t my box at all. Facebook itself was down. Got the \u0026ldquo;Account Temporarily Unavailable, your account is currently unavailable due to a site issue\u0026rdquo; message, same as everyone else. Checked their own status comments and there were people all over it at the same time, one guy saying \u0026ldquo;down for me to, website but not app,\u0026rdquo; another one saying it had been sitting like that for 30 minutes with no response. Classic Meta outage, nothing to do with me.\nSo two separate things were true at once. Facebook actually was broken for a while, and my box genuinely had no working IPv6 the entire time and never did. The IPv6 fix didn\u0026rsquo;t solve the Facebook problem, that one just fixed itself once Meta sorted their servers out. But it wasn\u0026rsquo;t wasted effort either. Every connection on this box is faster now because it\u0026rsquo;s not wasting 13ms trying a route that was never going to work in the first place.\nIf a site\u0026rsquo;s being weird in the browser and mobile\u0026rsquo;s fine, don\u0026rsquo;t assume it\u0026rsquo;s the site straight away, check both protocols with curl first. But also don\u0026rsquo;t assume it\u0026rsquo;s automatically your fault either. Sometimes it really is them, and you just happen to find a genuine problem on your own end while you\u0026rsquo;re in there looking. Worth fixing anyway.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/ipv6-dead-route-why-i-disabled-it/","summary":"\u003cp\u003eStarted with something dumb. Facebook wouldn\u0026rsquo;t load in Vivaldi or Firefox on the desktop. Worked fine on mobile. Figured it was a browser thing at first. Extension, cookies, the usual 💨 .\nWasn\u0026rsquo;t that.\u003c/p\u003e\n\u003ch2 id=\"why-i-thought-i-might-need-ipv6\"\u003ewhy i thought i might need ipv6\u003c/h2\u003e\n\u003cp\u003eRan a curl test forcing each protocol separately, just to rule stuff in or out:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -4 -I https://www.facebook.com\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -6 -I https://www.facebook.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIPv4 came back clean. \u003ccode\u003eHTTP/2 200\u003c/code\u003e, instant. IPv6 flat out refused to connect:\u003c/p\u003e","title":"Why I Killed IPv6 on my G4-fedora44"},{"content":"Fresh Ubuntu Server 26.04 install on the Folio. Nothing exotic \u0026hellip; wired NIC (enp0s25) sitting there with no cable in it, wifi doing all the actual work like it always does on a laptop. Should be a non-issue.\nBoot time said otherwise. Over 2 minutes. On a server. For nothing.\n$ systemd-analyze blame 2min 151ms systemd-networkd-wait-online.service Yep. There it is. Confirmed with the critical chain too \u0026hellip; network-online.target wasn\u0026rsquo;t hit until 2min 5.041s in. Two minutes just sitting there.\nwhy $ networkctl status enp0s25 ● 2: enp0s25 State: no-carrier (configuring) Online state: offline ... Required For Online: yes No cable. No carrier. Never going to come up. But it\u0026rsquo;s flagged as required, so wait-online sits there patiently waiting on an interface that is never, ever going to answer \u0026hellip; full 120 second timeout, every single boot, forever, until I fixed it.\nthe \u0026ldquo;fix\u0026rdquo; that does nothing Netplan\u0026rsquo;s got a key for exactly this. optional: true. Read the docs, seems dead simple:\nnetwork: ethernets: enp0s25: match: macaddress: d8:9d:67:33:98:d4 set-name: enp0s25 optional: true version: 2 wifis: wlo1: dhcp4: true netplan get confirms it took:\nmatch: macaddress: \u0026#34;d8:9d:67:33:98:d4\u0026#34; optional: true set-name: \u0026#34;enp0s25\u0026#34; Great, right? Should spit out RequiredForOnline=no into the generated config and I\u0026rsquo;m done.\nNope.\nnetplan apply Nothing. Deleted the generated file, forced a clean netplan generate. Still nothing:\n[Match] PermanentMACAddress=d8:9d:67:33:98:d4 Name=enp0s25 [Network] LinkLocalAddressing=ipv6 No [Link] block. No RequiredForOnline=no. Netplan happily takes the key, stores it, says nothing\u0026rsquo;s wrong, and then just\u0026hellip; doesn\u0026rsquo;t do anything with it. On netplan.io 1.2-1ubuntu5. No error. No warning. Nothing. Wasted a good chunk of time convinced I\u0026rsquo;d fat-fingered the YAML before I actually double and triple checked it was fine.\nwhat actually worked Stopped trusting netplan to translate it and went straight to systemd instead. Override wait-online directly, tell it to ignore that interface, done:\nsudo mkdir -p /etc/systemd/system/systemd-networkd-wait-online.service.d sudo tee /etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf \u0026gt; /dev/null \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; [Service] ExecStart= ExecStart=/lib/systemd/systemd-networkd-wait-online --ignore=enp0s25 EOF sudo systemctl daemon-reload Don\u0026rsquo;t skip that blank ExecStart= line \u0026hellip; systemd overrides append by default, so if you don\u0026rsquo;t clear the original first you\u0026rsquo;ll have two ExecStart lines fighting each other.\nRebooted.\nStartup finished in 8.159s (firmware) + 6.460s (loader) + 2.353s (kernel) + 3.296s (initrd) + 5.647s (userspace) = 25.917s Oh boy. At last. It actually worked. network-online.target at 5.112s. From over two minutes down to 25.9 seconds total.\nwhy this is the better fix anyway Even ignoring that netplan\u0026rsquo;s broken here \u0026hellip; this is the more solid way to do it regardless:\ndoesn\u0026rsquo;t care whether netplan\u0026rsquo;s YAML-to-systemd translation is working on whatever version you\u0026rsquo;ve got it\u0026rsquo;s explicit \u0026hellip; \u0026ldquo;ignore this interface,\u0026rdquo; full stop, no depending on a generated file being right survives netplan regenerating configs, package updates, whatever \u0026hellip; it\u0026rsquo;s systemd\u0026rsquo;s own override, netplan can\u0026rsquo;t touch it or break it later What i got from this If wait-online is eating your boot time and you\u0026rsquo;ve got a NIC that\u0026rsquo;s never coming up \u0026hellip; no cable, disabled dock, whatever \u0026hellip; don\u0026rsquo;t just trust that optional: true did what it says on the tin. Check it yourself:\ncat /run/systemd/network/10-netplan-\u0026lt;interface\u0026gt;.network No RequiredForOnline=no under [Link]? Stop fighting netplan. Override wait-online with --ignore=\u0026lt;interface\u0026gt; instead. One file, works every time, can\u0026rsquo;t quietly break on you later.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/netplan-optional-not-generating-required-for-online/","summary":"\u003cp\u003eFresh Ubuntu Server 26.04 install on the Folio. Nothing exotic \u0026hellip; wired NIC (\u003ccode\u003eenp0s25\u003c/code\u003e) sitting there with no cable in it, wifi doing all the actual work like it always does on a laptop. Should be a non-issue.\u003c/p\u003e\n\u003cp\u003eBoot time said otherwise. \u003cstrong\u003eOver 2 minutes.\u003c/strong\u003e On a server. For nothing.\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e$ systemd-analyze blame\n2min 151ms systemd-networkd-wait-online.service\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eYep. There it is. Confirmed with the critical chain too \u0026hellip; \u003ccode\u003enetwork-online.target\u003c/code\u003e wasn\u0026rsquo;t hit until \u003ccode\u003e2min 5.041s\u003c/code\u003e in. Two minutes just sitting there.\u003c/p\u003e","title":"netplan's optional: true Lied to Me"},{"content":"The actual goal I wanted my QNAP and T620 shares to show up as normal folders in Dolphin on every machine I use, not something I connect to manually every session. Real mountpoints, mounted at boot, showing up as actual directories. Two different systems here, so two genuinely different setups \u0026hellip; Fedora with a plain fstab, NixOS with a declarative config.\nFedora side (JackSparrow2 itself) \u0026hellip; the full block Step 1 \u0026hellip; every mountpoint directory, created up front:\nsudo mkdir -p \\ /mnt/nfs-data2 \\ /mnt/nfs-LINUXTWEAKS \\ /mnt/nfs-Public \\ /mnt/nfs-RELATIONSHIPS \\ /mnt/nfs-homes \\ /mnt/nfs-techs \\ /mnt/smb-LINUXTWEAKS \\ /mnt/smb-Public \\ /mnt/smb-RELATIONSHIPS \\ /mnt/smb-homes \\ /mnt/smb-techs I mount both NFS and SMB versions side by side \u0026hellip; genuinely useful for testing which protocol actually behaves better for a given workload, rather than committing to one blind.\nStep 2 \u0026hellip; back up fstab before touching it, every time, no exceptions:\nsudo cp /etc/fstab /etc/fstab.bak.$(date +%Y%m%d-%H%M%S) Cost nothing, saved me more than once.\nStep 3 \u0026hellip; a real credentials file for the SMB mounts, so the password isn\u0026rsquo;t sitting in plain fstab where anyone reading it can see it:\nsudo mkdir -p /etc/samba/creds sudo tee /etc/samba/creds/jacksparrow2 \u0026gt; /dev/null \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; username=tolga password=your-actual-samba-password EOF sudo chmod 600 /etc/samba/creds/jacksparrow2 sudo chown root:root /etc/samba/creds/jacksparrow2 600 and root:root matter here \u0026hellip; this file has a real password in it, no reason for it to be readable by anyone else on the box.\nStep 4 \u0026hellip; the actual fstab block, both protocols, every share:\nsudo tee -a /etc/fstab \u0026gt; /dev/null \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # -- MOUNTS ---- 192.168.0.xxx:/mnt/data2 /mnt/nfs-data2 nfs soft,timeo=30,retrans=2,noatime 0 0 192.168.0.xxx:/LINUXTWEAKS /mnt/nfs-LINUXTWEAKS nfs vers=3,soft,timeo=30,retrans=2,noatime 0 0 192.168.0.xxx:/Public /mnt/nfs-Public nfs vers=3,soft,timeo=30,retrans=2,noatime 0 0 192.168.0.xxx:/RELATIONSHIPS /mnt/nfs-RELATIONSHIPS nfs vers=3,soft,timeo=30,retrans=2,noatime 0 0 192.168.0.xxx:/homes /mnt/nfs-homes nfs vers=3,soft,timeo=30,retrans=2,noatime 0 0 192.168.0.xxx:/techs /mnt/nfs-techs nfs vers=3,soft,timeo=30,retrans=2,noatime 0 0 //192.168.0.xxx/LINUXTWEAKS /mnt/smb-LINUXTWEAKS cifs credentials=/etc/samba/creds/jacksparrow2,uid=1000,gid=1000,noatime,_netdev 0 0 //192.168.0.xxx/Public /mnt/smb-Public cifs credentials=/etc/samba/creds/jacksparrow2,uid=1000,gid=1000,noatime,_netdev 0 0 //192.168.0.xxx/RELATIONSHIPS /mnt/smb-RELATIONSHIPS cifs credentials=/etc/samba/creds/jacksparrow2,uid=1000,gid=1000,noatime,_netdev 0 0 //192.168.0.xxx/homes /mnt/smb-homes cifs credentials=/etc/samba/creds/jacksparrow2,uid=1000,gid=1000,noatime,_netdev 0 0 //192.168.0.xxx/techs /mnt/smb-techs cifs credentials=/etc/samba/creds/jacksparrow2,uid=1000,gid=1000,noatime,_netdev 0 0 EOF soft,timeo=30,retrans=2 on every NFS line \u0026hellip; the box that taught me why: a hard mount that stalls during boot can hang the entire startup sequence in a state you can\u0026rsquo;t even kill. _netdev on the CIFS lines so systemd knows to wait for networking before attempting them.\nStep 5 \u0026hellip; apply and test:\nsudo systemctl daemon-reload sudo mount -a Step 6 \u0026hellip; actually verify, both protocols:\nmount | grep -E \u0026#39;nfs-|smb-\u0026#39; df -h | grep -E \u0026#39;nfs-|smb-\u0026#39; df -h alongside mount gives me real used/available space per share, not just a yes/no on whether it\u0026rsquo;s attached \u0026hellip; a mount can show as active while genuinely broken, df reporting real numbers is a better signal that it\u0026rsquo;s actually working.\nNixOS side (my desktop) \u0026hellip; the config, not a plain fstab NixOS doesn\u0026rsquo;t have a hand-edited /etc/fstab \u0026hellip; fileSystems entries in configuration.nix (or an imported module) get compiled into real systemd mount units at build time. This is the actual config I run, split across two files.\nThe main module, importing a T620-specific submodule and handling direct QNAP NFS mounts:\n{ config , lib , pkgs , username , ... }: let nfsOpts = [ \u0026#34;x-systemd.after=network-online.target\u0026#34; \u0026#34;x-systemd.automount\u0026#34; \u0026#34;x-systemd.idle-timeout=600\u0026#34; \u0026#34;x-systemd.mount-timeout=30\u0026#34; \u0026#34;x-systemd.requires=network-online.target\u0026#34; \u0026#34;_netdev\u0026#34; \u0026#34;noauto\u0026#34; \u0026#34;nofail\u0026#34; \u0026#34;vers=3\u0026#34; \u0026#34;rw\u0026#34; \u0026#34;soft\u0026#34; \u0026#34;timeo=30\u0026#34; ]; in { imports = [ ./T620 ]; systemd.tmpfiles.rules = [ \u0026#34;d /mnt/nfs-Relationships 0755 ${username} ${username} -\u0026#34; \u0026#34;d /mnt/nfs-linuxtweaks 0755 ${username} ${username} -\u0026#34; \u0026#34;d /mnt/nfs-public 0755 ${username} ${username} -\u0026#34; \u0026#34;d /mnt/nfs-techs 0755 ${username} ${username} -\u0026#34; ]; fileSystems = { \u0026#34;/mnt/nfs-public\u0026#34; = { device = \u0026#34;192.168.0.xxx:/Public\u0026#34;; fsType = \u0026#34;nfs\u0026#34;; options = nfsOpts; }; \u0026#34;/mnt/nfs-linuxtweaks\u0026#34; = { device = \u0026#34;192.168.0.xxx:/LINUXTWEAKS\u0026#34;; fsType = \u0026#34;nfs\u0026#34;; options = nfsOpts; }; \u0026#34;/mnt/nfs-techs\u0026#34; = { device = \u0026#34;192.168.0.xxx:/techs\u0026#34;; fsType = \u0026#34;nfs\u0026#34;; options = nfsOpts; }; \u0026#34;/mnt/nfs-Relationships\u0026#34; = { device = \u0026#34;192.168.0.xxx:/RELATIONSHIPS\u0026#34;; fsType = \u0026#34;nfs\u0026#34;; options = nfsOpts; }; }; } # rebuild with the automount fix if it\u0026#39;s ever stuck on a stale unit: # sudo systemctl stop \u0026#39;mnt-nfs\\x2d*.automount\u0026#39; 2\u0026gt;/dev/null || true \u0026amp;\u0026amp; sudo nixos-rebuild switch # # QNAP-side permissions needed first for Public and LINUXTWEAKS: # ssh admin@192.168.0.xxx # chown -R tolga:everyone /share/Public /share/LINUXTWEAKS # chmod -R 777 /share/Public /share/LINUXTWEAKS The T620 submodule \u0026hellip; SMB shares straight off JackSparrow2 itself, kept separate because it\u0026rsquo;s genuinely a different backend and different protocol entirely:\n{ config, lib, pkgs, username, ... }: let t620Host = \u0026#34;192.168.0.xxx\u0026#34;; # JackSparrow2 LAN IP ... check if it changes credsFile = \u0026#34;/etc/nixos/modules/mnt/T620/credentials\u0026#34;; smbOpts = [ \u0026#34;credentials=${credsFile}\u0026#34; \u0026#34;uid=${username}\u0026#34; \u0026#34;gid=users\u0026#34; \u0026#34;vers=3.1.1\u0026#34; \u0026#34;actimeo=60\u0026#34; \u0026#34;_netdev\u0026#34; \u0026#34;nofail\u0026#34; ]; in { systemd.tmpfiles.rules = [ \u0026#34;d /mnt/t620-1TB 0755 ${username} users -\u0026#34; \u0026#34;d /mnt/t620-home 0755 ${username} users -\u0026#34; \u0026#34;d /mnt/t620-linuxtweaks 0755 ${username} users -\u0026#34; \u0026#34;d /mnt/t620-public 0755 ${username} users -\u0026#34; \u0026#34;d /mnt/t620-public-qnap 0755 ${username} users -\u0026#34; \u0026#34;d /mnt/t620-relationships 0755 ${username} users -\u0026#34; \u0026#34;d /mnt/t620-techs 0755 ${username} users -\u0026#34; ]; fileSystems = { \u0026#34;/mnt/t620-1TB\u0026#34; = { device = \u0026#34;//${t620Host}/1TB_Storage\u0026#34;; fsType = \u0026#34;cifs\u0026#34;; options = smbOpts; }; \u0026#34;/mnt/t620-public\u0026#34; = { device = \u0026#34;//${t620Host}/Public\u0026#34;; fsType = \u0026#34;cifs\u0026#34;; options = smbOpts; }; \u0026#34;/mnt/t620-public-qnap\u0026#34; = { device = \u0026#34;//${t620Host}/Public_QNAP\u0026#34;; fsType = \u0026#34;cifs\u0026#34;; options = smbOpts; }; \u0026#34;/mnt/t620-relationships\u0026#34; = { device = \u0026#34;//${t620Host}/RELATIONSHIPS\u0026#34;; fsType = \u0026#34;cifs\u0026#34;; options = smbOpts; }; \u0026#34;/mnt/t620-linuxtweaks\u0026#34; = { device = \u0026#34;//${t620Host}/LINUXTWEAKS\u0026#34;; fsType = \u0026#34;cifs\u0026#34;; options = smbOpts; }; \u0026#34;/mnt/t620-techs\u0026#34; = { device = \u0026#34;//${t620Host}/techs\u0026#34;; fsType = \u0026#34;cifs\u0026#34;; options = smbOpts; }; \u0026#34;/mnt/t620-home\u0026#34; = { device = \u0026#34;//${t620Host}/${username}\u0026#34;; fsType = \u0026#34;cifs\u0026#34;; options = smbOpts; }; }; } Genuinely important detail I got wrong the first time: actimeo=60 was added deliberately after finding Dolphin felt slow browsing these shares with the CIFS default caching behaviour \u0026hellip; 60 seconds of attribute caching made a real, noticeable difference on repeat folder access without introducing anything unsafe for how I actually use these shares.\nThe credentials file the T620 module points at, kept outside the Nix store entirely on purpose so the password never ends up readable in /nix/store:\nsudo mkdir -p /etc/nixos/modules/mnt/T620 sudo tee /etc/nixos/modules/mnt/T620/credentials \u0026gt; /dev/null \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; username=tolga password=your-actual-samba-password domain=WORKGROUP EOF sudo chmod 600 /etc/nixos/modules/mnt/T620/credentials Apply it:\nsudo nixos-rebuild switch Verify:\nmount | grep t620 mount | grep nfs- ls /mnt/t620-public Getting it into Dolphin Once either the Fedora fstab lines or the NixOS fileSystems config are actually mounted, there\u0026rsquo;s nothing left to do for Dolphin specifically \u0026hellip; the mountpoints just show up as real folders at whatever path you defined, /mnt/smb-Public or /mnt/t620-public, no different from any other directory on disk. No need to browse smb:// manually once it\u0026rsquo;s properly mounted this way; that\u0026rsquo;s only for ad-hoc access to a share you haven\u0026rsquo;t set up a permanent mount for.\nWhat actually mattered Back up fstab before every edit. Use soft with a real timeout on every NFS mount, never plain hard. Keep SMB credentials in their own locked-down file, never inline in fstab or committed into the Nix store. On NixOS specifically, actimeo on CIFS mounts is worth tuning if browsing feels sluggish \u0026hellip; the default caching behaviour isn\u0026rsquo;t wrong, just conservative. And verify with both mount and df -h, not one or the other, since a mount reporting active isn\u0026rsquo;t proof it\u0026rsquo;s actually serving real data.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/fstab-mount-dolphin-guide/","summary":"\u003ch2 id=\"the-actual-goal\"\u003eThe actual goal\u003c/h2\u003e\n\u003cp\u003eI wanted my QNAP and T620 shares to show up as normal folders in\nDolphin on every machine I use, not something I connect to manually\nevery session. Real mountpoints, mounted at boot, showing up as actual\ndirectories. Two different systems here, so two genuinely different\nsetups \u0026hellip; Fedora with a plain fstab, NixOS with a declarative config.\u003c/p\u003e\n\u003ch2 id=\"fedora-side-jacksparrow2-itself--the-full-block\"\u003eFedora side (JackSparrow2 itself) \u0026hellip; the full block\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eStep 1 \u0026hellip; every mountpoint directory, created up front:\u003c/strong\u003e\u003c/p\u003e","title":"Mounting NFS and SMB Shares So They Actually Show Up in Dolphin"},{"content":"Why I\u0026rsquo;m writing this one down properly This site you\u0026rsquo;re reading right now runs on the same T620 as everything else \u0026hellip; the NAS, the Samba shares, all of it. I got it working, but not in a straight line, and I want an actual record of the steps that worked, not a cleaned-up version that pretends I got it right first try. If I ever rebuild this box, this is the post I want to follow.\nStep 1 \u0026hellip; nginx, listening locally only sudo dnf install -y nginx sudo mkdir -p /var/www/kingtolga/images sudo chown -R nginx:nginx /var/www/kingtolga I deliberately bound nginx to localhost only, not 0.0.0.0 \u0026hellip; this site was never meant to be reachable by opening a port on my router.\nserver { listen 127.0.0.1:8080; server_name _; root /var/www/kingtolga; index index.html; } Step 2 \u0026hellip; Tailscale Funnel instead of port forwarding I run Tailscale on every machine I own already, so instead of opening ports on my router and messing with dynamic DNS, I used Funnel to expose this one nginx instance to the actual public internet, with Tailscale handling TLS automatically.\nsudo tailscale funnel 8080 First run told me Funnel wasn\u0026rsquo;t enabled on my tailnet yet and gave me a link to approve it in the admin console. I did that, then ran it again \u0026hellip; but as --bg this time, because running it in the foreground only lasts as long as that terminal session stays open:\nsudo tailscale funnel --bg 8080 tailscale funnel status I also opened http/https in firewalld at this point, out of habit \u0026hellip; that was wrong, and I removed it later. Funnel doesn\u0026rsquo;t need those ports open on the host firewall at all; Tailscale\u0026rsquo;s own daemon handles the public-facing side and proxies internally. Left open, they were just doing nothing useful:\nsudo firewall-cmd --permanent --remove-service=http sudo firewall-cmd --permanent --remove-service=https sudo firewall-cmd --reload Step 3 \u0026hellip; the first real wall: SELinux, 403 on everything Put a test image and a hand-written index.html straight into /var/www/kingtolga. Got a flat 403 Forbidden from nginx, on a file with completely correct Unix permissions. getenforce confirmed SELinux was enforcing, and ls -Zd showed the directory carrying var_t \u0026hellip; the wrong context. nginx\u0026rsquo;s SELinux policy only allows it to read httpd_sys_content_t.\nsudo semanage fcontext -a -t httpd_sys_content_t \u0026#34;/var/www/kingtolga(/.*)?\u0026#34; sudo restorecon -Rv /var/www/kingtolga That fixed it immediately. I\u0026rsquo;d hit this exact class of problem before on this same box with Samba and SFTP sharing a directory, so at least this time I recognised it fast instead of chasing Unix permissions for twenty minutes first.\nStep 4 \u0026hellip; deciding a hand-written index.html wasn\u0026rsquo;t enough I originally wanted \u0026ldquo;a page and some images.\u0026rdquo; Then I actually wanted posts, categories, tags \u0026hellip; a real blog structure, not a folder with a link in it. That meant Hugo, not a static HTML file I was editing by hand.\nsudo dnf install -y hugo git cd ~/hugo hugo new site kingtolga cd kingtolga git init git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod Step 5 \u0026hellip; the config that actually stuck baseURL = \u0026#34;https://jacksparrow2.tail9e758e.ts.net/\u0026#34; title = \u0026#34;Tolgas LinuxTweaks\u0026#34; theme = \u0026#34;PaperMod\u0026#34; paginate = 5 [taxonomies] category = \u0026#34;categories\u0026#34; tag = \u0026#34;tags\u0026#34; [params] ShowReadingTime = true ShowPostNavLinks = true ShowBreadCrumbs = true ShowShareButtons = false ShowToc = true TocOpen = false ShowCodeCopyButtons = true favicon = \u0026#34;/img/favicon.png\u0026#34; label.text = \u0026#34;Tolgas LinuxTweaks\u0026#34; label.icon = \u0026#34;/img/favicon.png\u0026#34; label.iconHeight = 35 [[menu.main]] name = \u0026#34;Posts\u0026#34; url = \u0026#34;/posts/\u0026#34; weight = 10 [[menu.main]] name = \u0026#34;Categories\u0026#34; url = \u0026#34;/categories/\u0026#34; weight = 40 [[menu.main]] name = \u0026#34;Tags\u0026#34; url = \u0026#34;/tags/\u0026#34; weight = 50 Step 6 \u0026hellip; the front matter mistake that broke the first build Hugo generates TOML front matter by default now, not YAML \u0026hellip; I tried pasting YAML-style content into a TOML block and the build failed with an unmarshal error. Also left draft = true in place the first time and wondered why nothing showed up on the site. Both fixed the same way, just paying attention to the actual generated template:\n+++ date = \u0026#39;2026-07-18T13:39:47+08:00\u0026#39; draft = false title = \u0026#39;First Post\u0026#39; categories = [\u0026#39;General\u0026#39;] tags = [\u0026#39;homelab\u0026#39;] +++ Step 7 \u0026hellip; the thing I added, then ripped straight back out I tried adding a cover image to every post, using my LinuxTweaks logo, thinking it\u0026rsquo;d look like a nice banner at the top of each post:\ncover.image = \u0026#34;/img/favicon.png\u0026#34; cover.alt = \u0026#34;LinuxTweaks\u0026#34; cover.relative = false It looked terrible. A small square logo stretched to fill the entire width of a post banner is not a good look \u0026hellip; genuinely huge and distorted on every single post. I pulled it back out of every post\u0026rsquo;s front matter entirely:\nfor f in ~/hugo/kingtolga/content/posts/*.md; do sed -i \u0026#39;/^cover\\./d\u0026#39; \u0026#34;$f\u0026#34; done The logo stayed exactly where it actually belongs \u0026hellip; small, in the site header, via label.icon in hugo.toml, not stretched across every post. Worth knowing before you try the same thing: a square icon is not a banner image, and Hugo/PaperMod will happily stretch it like one if you tell it to.\nStep 8 \u0026hellip; deploying, and making it repeatable Building and copying the output by hand a few times was enough to know I wanted this scripted properly, not retyped every time I wrote a post:\nhugo --minify sudo rm -rf /var/www/kingtolga/* sudo cp -r ~/hugo/kingtolga/public/* /var/www/kingtolga/ sudo chown -R nginx:nginx /var/www/kingtolga sudo restorecon -Rv /var/www/kingtolga That\u0026rsquo;s the whole deploy sequence \u0026hellip; build, wipe the web root, copy the fresh build in, fix ownership, fix SELinux context, every single time. Wrapped into one script so publishing a new post is one command, not five.\nThe actual order, if I ever do this again nginx, bound to localhost only Tailscale Funnel, --bg, no firewall ports opened for it Fix SELinux context on the web root before touching anything else \u0026hellip; this bites you regardless of what\u0026rsquo;s actually serving the content Hugo site + PaperMod theme Config with taxonomies and menu set up from the start, not bolted on later TOML front matter, draft = false, checked every time Logo goes in the header via label.icon. Not as a per-post cover. Script the deploy sequence before writing the second post, not after getting tired of retyping it five times How I actually write and publish a post, every time Not through nano, not through a web editor \u0026hellip; straight from the terminal with a heredoc, because it\u0026rsquo;s faster than opening a file, typing front matter by hand, and saving:\ncat \u0026gt; ~/hugo/kingtolga/content/posts/whatever-the-post-is-called.md \u0026lt;\u0026lt; \u0026#39;BLOGPOST\u0026#39; +++ date = \u0026#39;2026-07-18T15:00:00+08:00\u0026#39; draft = false title = \u0026#39;Post Title Here\u0026#39; description = \u0026#34;One sentence describing what the post actually covers, like, worship kingtolga.\u0026#34; categories = [\u0026#39;God\u0026#39;] tags = [\u0026#39;relevant\u0026#39;, \u0026#39;tags\u0026#39;, \u0026#39;here\u0026#39;] +++ Actual content goes here. BLOGPOST Whole post, front matter and all, written and saved in one command \u0026hellip; no editor navigation, no accidentally leaving draft = true from a template I forgot to change. The 'BLOGPOST' at the start and end is just a marker telling bash where the pasted content begins and ends; call it anything, as long as the opening and closing markers match.\nThe script that makes any of this actually go live None of the above does anything by itself \u0026hellip; it just creates a markdown file sitting in ~/hugo/kingtolga/content/posts/. Getting it onto the actual public site is hugo-deploy.sh, the script built specifically to collapse the build-and-publish sequence from step 8 above into one command:\nsudo /home/tolga/hugo-deploy.sh That single call runs hugo --minify, wipes and repopulates /var/www/kingtolga, fixes ownership back to nginx:nginx, and reapplies the SELinux context \u0026hellip; every time, in the right order, without me needing to remember the five separate steps or the exact restorecon flags months from now. Write the post with the heredoc above, run the one script, done.\nWhere hugo-deploy.sh actually lives, and how I run it It landed in /home/tolga/my-scripts/hugo-deploy.sh the first time I copied it over. I moved it into /usr/local/bin so it\u0026rsquo;s a real installed command instead of a file sitting in Downloads:\nsudo mv /home/tolga/my-scripts/hugo-deploy.sh /usr/local/bin/hugo-deploy sudo chmod +x /usr/local/bin/hugo-deploy From that point on, running it is just:\nsudo ./hugo-deploy.sh If I ever forget where it is, this finds it:\nfind / -name \u0026#34;hugo-deploy*\u0026#34; 2\u0026gt;/dev/null The actual script, full contents #!/usr/bin/env bash # ============================================================================= # hugo-deploy.sh ... JackSparrow2 ... kingtolga # # Rebuilds and redeploys the Hugo site to nginx\u0026#39;s web root, with the correct # SELinux relabel. I run this any time after editing/adding posts. # # Usage: # sudo ./hugo-deploy.sh # ============================================================================= set -uo pipefail GREEN=\u0026#39;\\033[0;32m\u0026#39;; YELLOW=\u0026#39;\\033[1;33m\u0026#39;; RED=\u0026#39;\\033[0;31m\u0026#39;; NC=\u0026#39;\\033[0m\u0026#39; info() { echo -e \u0026#34;${YELLOW}[*]${NC} $1\u0026#34;; } ok() { echo -e \u0026#34;${GREEN}[OK]${NC} $1\u0026#34;; } err() { echo -e \u0026#34;${RED}[!]${NC} $1\u0026#34;; } if [[ $EUID -ne 0 ]]; then err \u0026#34;Run as root: sudo $0\u0026#34; exit 1 fi REAL_USER=\u0026#34;${SUDO_USER:-tolga}\u0026#34; REAL_HOME=$(getent passwd \u0026#34;$REAL_USER\u0026#34; | cut -d: -f6) HUGO_ROOT=\u0026#34;${REAL_HOME}/hugo/kingtolga\u0026#34; WEB_ROOT=\u0026#34;/var/www/kingtolga\u0026#34; NGINX_PORT=\u0026#34;8080\u0026#34; if [[ ! -d \u0026#34;$HUGO_ROOT\u0026#34; ]]; then err \u0026#34;No Hugo site found at ${HUGO_ROOT}\u0026#34; exit 1 fi info \u0026#34;Building site...\u0026#34; su \u0026#34;$REAL_USER\u0026#34; -c \u0026#34;cd \u0026#39;$HUGO_ROOT\u0026#39; \u0026amp;\u0026amp; hugo --minify\u0026#34; \\ \u0026amp;\u0026amp; ok \u0026#34;Build succeeded\u0026#34; \\ || { err \u0026#34;hugo build failed ... nothing was deployed\u0026#34;; exit 1; } info \u0026#34;Deploying to ${WEB_ROOT}...\u0026#34; rm -rf \u0026#34;${WEB_ROOT:?}\u0026#34;/* cp -r \u0026#34;${HUGO_ROOT}/public/\u0026#34;* \u0026#34;$WEB_ROOT/\u0026#34; chown -R nginx:nginx \u0026#34;$WEB_ROOT\u0026#34; restorecon -Rv \u0026#34;$WEB_ROOT\u0026#34; \u0026gt; /dev/null ok \u0026#34;Deployed\u0026#34; info \u0026#34;Verifying...\u0026#34; sleep 1 if curl -sf \u0026#34;http://127.0.0.1:${NGINX_PORT}/\u0026#34; \u0026gt; /dev/null; then ok \u0026#34;Site is live and responding locally\u0026#34; else err \u0026#34;Local check failed ... run: curl -I http://127.0.0.1:${NGINX_PORT}/\u0026#34; fi Nothing in it is a mystery at this point \u0026hellip; REAL_USER/REAL_HOME resolve who actually owns the Hugo source files (since the script runs as root via sudo but the site itself needs to be built as my regular user, not root, or file ownership under ~/hugo gets messed up). Everything past that is just the same five manual steps from earlier in this post, in the same order, every time.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/hugo-on-t620-final-setup/","summary":"\u003ch2 id=\"why-im-writing-this-one-down-properly\"\u003eWhy I\u0026rsquo;m writing this one down properly\u003c/h2\u003e\n\u003cp\u003eThis site you\u0026rsquo;re reading right now runs on the same T620 as everything\nelse \u0026hellip; the NAS, the Samba shares, all of it. I got it working, but not\nin a straight line, and I want an actual record of the steps that\nworked, not a cleaned-up version that pretends I got it right first\ntry. If I ever rebuild this box, this is the post I want to follow.\u003c/p\u003e","title":"Getting Hugo Actually Working on the T620 ... the Real Steps, in Order"},{"content":"Why Tailscale, not a real VPN server I looked at running my own WireGuard setup by hand \u0026hellip; port forwarding, managing keys myself, keeping track of which peer is which. Tailscale does all of that for you, on top of WireGuard, and the actual setup on each machine ends up being a handful of lines. Every device I own is on the same tailnet now: the T620 server, my NixOS desktop, and \u0026hellip; the one that surprised me \u0026hellip; a MacBook Air 6,2 from 2014 that has no business running anything modern smoothly, and does it fine anyway.\nThe whole NixOS config, genuinely this short { config, pkgs, lib, ... }: { services.tailscale.enable = true; networking.firewall = { enable = true; trustedInterfaces = [ \u0026#34;tailscale0\u0026#34; ]; allowedTCPPorts = [ 22 ]; allowedUDPPorts = [ 41641 ]; }; powerManagement.resumeCommands = \u0026#39;\u0026#39; ${pkgs.systemd}/bin/systemctl restart tailscaled \u0026#39;\u0026#39;; } That\u0026rsquo;s it. Three real settings, not a wall of options.\nBreaking down each piece services.tailscale.enable = true; \u0026hellip; installs and enables the daemon. On a normal distro this is a package install plus a systemd enable; on NixOS it\u0026rsquo;s one line that handles both.\ntrustedInterfaces = [ \u0026quot;tailscale0\u0026quot; ] \u0026hellip; this is the one that actually matters and the one I got wrong on the T620 the first time around. Tailscale creates its own virtual network interface, and without explicitly trusting it in the firewall, traffic coming through the tailnet gets treated the same as traffic from the open internet \u0026hellip; meaning things that should just work over Tailscale (SSH, Samba, SFTP) get blocked or, worse, silently forced through a relay because the direct connection can\u0026rsquo;t establish properly. One line, and every service already running is reachable over the tailnet without touching individual port rules for each thing.\nallowedTCPPorts = [ 22 ] \u0026hellip; SSH, opened normally, nothing Tailscale-specific about this line, just there because I want SSH reachable.\nallowedUDPPorts = [ 41641 ] \u0026hellip; this is Tailscale\u0026rsquo;s own port for direct peer-to-peer connection negotiation (NAT hole-punching). Without it open, Tailscale still works, but every connection falls back to relaying through Tailscale\u0026rsquo;s own DERP servers instead of connecting directly device-to-device \u0026hellip; meaning real, measurable extra latency for absolutely no reason, every single time. I found this out by comparing tailscale status output between two machines and noticing one said direct and the other said relay for the exact same peer, at the exact same location. This one line fixed the difference completely.\nThe suspend/resume fix, and why it\u0026rsquo;s there powerManagement.resumeCommands = \u0026#39;\u0026#39; ${pkgs.systemd}/bin/systemctl restart tailscaled \u0026#39;\u0026#39;; Tailscale\u0026rsquo;s daemon doesn\u0026rsquo;t reliably reconnect its tunnel on its own after a laptop wakes from sleep \u0026hellip; the underlying network interface comes back, but the tailscaled process itself sometimes just sits there with a dead connection until something kicks it. This forces a clean restart of the daemon specifically at resume, so I never wake the machine up to find Tailscale silently disconnected and have to notice and fix it manually.\nThe MacBook Air, and why it just works too The genuinely surprising part wasn\u0026rsquo;t the NixOS desktop or the Fedora server \u0026hellip; both are machines I expected to configure carefully. It was installing Tailscale on a 2014 MacBook Air 6,2 with 4GB of RAM and expecting some kind of fight to get it running smoothly. There wasn\u0026rsquo;t one. Install the client, log in, done \u0026hellip; same tailnet, same access to every other machine, no meaningful resource cost on hardware that struggles with plenty of far lighter software.\nThat\u0026rsquo;s the actual point of this whole setup for me. It\u0026rsquo;s not one clever config on one important machine \u0026hellip; it\u0026rsquo;s the same trivial setup, repeated across genuinely different hardware and different operating systems, and every single one of them just becomes another device on the same private network with zero port forwarding on my actual router and zero manual key management on my end.\nWhat I\u0026rsquo;d actually tell someone setting this up Don\u0026rsquo;t skip the firewall interface trust line if you\u0026rsquo;re on NixOS or anything with a default-deny firewall \u0026hellip; that one line is the difference between \u0026ldquo;Tailscale is installed\u0026rdquo; and \u0026ldquo;Tailscale actually lets your other services through.\u0026rdquo; And check tailscale status on both ends of a connection you care about being fast \u0026hellip; if it says relay instead of showing a direct IP, that\u0026rsquo;s almost always a firewall/UDP port problem, not something wrong with Tailscale itself.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/tailscale-everywhere/","summary":"\u003ch2 id=\"why-tailscale-not-a-real-vpn-server\"\u003eWhy Tailscale, not a real VPN server\u003c/h2\u003e\n\u003cp\u003eI looked at running my own WireGuard setup by hand \u0026hellip; port forwarding,\nmanaging keys myself, keeping track of which peer is which. Tailscale\ndoes all of that for you, on top of WireGuard, and the actual setup on\neach machine ends up being a handful of lines. Every device I own is on\nthe same tailnet now: the T620 server, my NixOS desktop, and \u0026hellip; the one\nthat surprised me \u0026hellip; a MacBook Air 6,2 from 2014 that has no business\nrunning anything modern smoothly, and does it fine anyway.\u003c/p\u003e","title":"Tailscale on Everything I Own ... Even a 2014 MacBook Air"},{"content":"Same symptom, completely different machine I already wrote about the remount script I built for my NixOS desktop \u0026hellip; clearing stale network mount units through systemd when a share drops.\nThis is the other half of that same story, except this one lives on JackSparrow2 itself (the T620 server) and fixes a genuinely different failure, even though the symptom looks identical from the client side: a share that used to work suddenly doesn\u0026rsquo;t.\nGetting it onto the actual server scp qnap-remount.sh tolga@192.168.0.xxx:~/ ssh tolga@192.168.0.xxx sudo mv ~/qnap-remount.sh /usr/local/bin/qnap-remount sudo chmod +x /usr/local/bin/qnap-remount Copied over, dropped into /usr/local/bin without the .sh extension so it reads like a proper installed command rather than a script sitting in a folder somewhere.\nThe actual root cause this one fixes The NixOS script deals with the client side going stale. This one deals with something happening between JackSparrow2 and the QNAP behind it \u0026hellip; a hiccup in the QNAP\u0026rsquo;s own NFS export table, or Samba on the T620 holding onto file handles that pointed at a mount that\u0026rsquo;s since dropped and come back differently. Different layer, different fix needed.\nChecking the QNAP is even reachable before doing anything nas_ip=\u0026#34;192.168.0.xxx\u0026#34; step \u0026#34;Checking QNAP is reachable\u0026#34; if ! ping -c1 -W2 \u0026#34;$nas_ip\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then err \u0026#34;QNAP ($nas_ip) is not responding to ping.\u0026#34; exit 1 fi No point trying to fix mounts to a device that\u0026rsquo;s genuinely offline \u0026hellip; this exits immediately with a clear message instead of grinding through umount/remount attempts against a dead target and producing confusing output.\nOnly touching mounts that are actually stale, not everything for d in \u0026#34;$data_root\u0026#34;/*; do [[ -d \u0026#34;$d\u0026#34; ]] || continue if mountpoint -q \u0026#34;$d\u0026#34;; then if ! ls \u0026#34;$d\u0026#34; \u0026amp;\u0026gt;/dev/null; then warn \u0026#34;$d looks stale, forcing unmount\u0026#34; sudo umount -f -l \u0026#34;$d\u0026#34; 2\u0026gt;\u0026amp;1 fi fi done This is the important bit \u0026hellip; it checks each mount two ways, not one. mountpoint -q confirms it\u0026rsquo;s genuinely mounted at all, but that alone doesn\u0026rsquo;t prove it\u0026rsquo;s working. ls \u0026quot;$d\u0026quot; is the real test \u0026hellip; a stale NFS handle will still show as mounted while completely failing to list its own contents. Only mounts that fail both checks get force-unmounted; anything actually healthy gets left alone.\nSince this box has an fstab, the remount step is simpler Unlike the NixOS side, JackSparrow2 runs plain Fedora with a real /etc/fstab, so remounting is the standard, boring command:\nstep \u0026#34;Remounting via /etc/fstab\u0026#34; sudo mount -a The step that isn\u0026rsquo;t obvious until you\u0026rsquo;ve actually hit this step \u0026#34;Restarting Samba (smbd holds cached handles from before the remount)\u0026#34; sudo systemctl restart smb nmb This is the part that cost me real time the first few times this happened. Remounting the underlying NFS path from the QNAP doesn\u0026rsquo;t automatically mean Samba picks up the fresh mount \u0026hellip; smbd can keep serving through a cached file handle that points at the old mount instance, meaning clients connecting via Samba still see a broken share even though the NFS mount underneath is genuinely fine again. Restarting smb/nmb forces it to re-establish everything fresh against the now-working mount.\nKnowing when the script genuinely can\u0026rsquo;t fix it if [[ \u0026#34;$failed\u0026#34; -eq 0 ]]; then msg \u0026#34;All shares recovered.\u0026#34; else warn \u0026#34;Some shares are still broken.\u0026#34; echo \u0026#34; This usually means the QNAP\u0026#39;s own NFS export table needs a reboot:\u0026#34; echo \u0026#34; ssh admin@${nas_ip}\u0026#34; echo \u0026#34; cat /proc/fs/nfsd/exports # if empty, that\u0026#39;s the problem\u0026#34; echo \u0026#34; Control Panel -\u0026gt; System -\u0026gt; Power -\u0026gt; Restart\u0026#34; fi I added this after actually hitting the case where the problem wasn\u0026rsquo;t on JackSparrow2\u0026rsquo;s side at all \u0026hellip; the QNAP\u0026rsquo;s own export table had gone empty, which no amount of remounting or restarting Samba on the client side can fix, because there\u0026rsquo;s nothing valid being exported to reconnect to. The script tells you exactly where to look instead of silently failing or, worse, looking like it succeeded when it didn\u0026rsquo;t actually fix anything.\nRunning it sudo qnap-remount One command, checks the QNAP is alive, only touches mounts that are genuinely broken, remounts through the real fstab, restarts Samba so cached handles don\u0026rsquo;t linger, and tells me plainly if the real problem is actually on the QNAP\u0026rsquo;s end rather than pretending it fixed something it didn\u0026rsquo;t.\nTwo scripts, same instinct Different machine, different mount technology, different actual root cause \u0026hellip; but the same underlying lesson both times: don\u0026rsquo;t assume a mount reporting as \u0026ldquo;active\u0026rdquo; means it\u0026rsquo;s actually working. Check by listing real content, every time, and only act on what\u0026rsquo;s genuinely broken.\nscp qnap-remount.sh tolga@192.168.0.xxx:~/ ssh tolga@192.168.0.xxx sudo mv ~/qnap-remount.sh /usr/local/bin/qnap-remount sudo chmod +x /usr/local/bin/qnap-remount scp ~/Downloads/qnap-remount.sh tolga@192.168.0.xxx:~/ cat /usr/local/bin/qnap-remount #!/usr/bin/env bash # ============================================================================= # qnap-remount.sh # Tolga Erok # Version : 1.0 # Date : 11 Jul 2026 # # Clears stale NFS mounts from QNAP (192.168.0.xxx) on JackSparrow2 and # restarts Samba so shares re-serve cleanly. Covers the exact failure # pattern hit repeatedly today: stale file handles after a QNAP export # table hiccup, and smbd holding broken cached paths afterward. # # Does NOT touch the QNAP itself -- if `cat /proc/fs/nfsd/exports` on the # QNAP is empty, that\u0026#39;s a server-side export table problem and needs a # full QNAP reboot (Control Panel -\u0026gt; System -\u0026gt; Power -\u0026gt; Restart), not # anything this script can fix from the client side. # ============================================================================= set -uo pipefail red=\u0026#39;\\e[31m\u0026#39;; grn=\u0026#39;\\e[32m\u0026#39;; yel=\u0026#39;\\e[33m\u0026#39;; cyn=\u0026#39;\\e[36m\u0026#39;; rst=\u0026#39;\\e[0m\u0026#39;; bld=\u0026#39;\\e[1m\u0026#39; msg() { echo -e \u0026#34;${grn}${bld}[ OK ]${rst} $*\u0026#34;; } info() { echo -e \u0026#34;${cyn}${bld}[ INFO ]${rst} $*\u0026#34;; } warn() { echo -e \u0026#34;${yel}${bld}[ WARN ]${rst} $*\u0026#34;; } err() { echo -e \u0026#34;${red}${bld}[ FAIL ]${rst} $*\u0026#34;; } step() { echo -e \u0026#34;\\n${yel}${bld} ──► $*${rst}\u0026#34;; } nas_ip=\u0026#34;192.168.0.xxx\u0026#34; data_root=\u0026#34;/mnt/data\u0026#34; step \u0026#34;Checking QNAP is reachable\u0026#34; if ! ping -c1 -W2 \u0026#34;$nas_ip\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then err \u0026#34;QNAP ($nas_ip) is not responding to ping.\u0026#34; echo \u0026#34; Check it\u0026#39;s powered on and on the network before continuing.\u0026#34; exit 1 fi msg \u0026#34;QNAP responding\u0026#34; step \u0026#34;Current NFS mounts from QNAP\u0026#34; mount | grep \u0026#34;$nas_ip\u0026#34; || warn \u0026#34;No NFS mounts from $nas_ip currently active\u0026#34; step \u0026#34;Unmounting anything stale\u0026#34; for d in \u0026#34;$data_root\u0026#34;/*; do [[ -d \u0026#34;$d\u0026#34; ]] || continue if mountpoint -q \u0026#34;$d\u0026#34;; then if ! ls \u0026#34;$d\u0026#34; \u0026amp;\u0026gt;/dev/null; then warn \u0026#34;$d looks stale, forcing unmount\u0026#34; sudo umount -f -l \u0026#34;$d\u0026#34; 2\u0026gt;\u0026amp;1 fi fi done step \u0026#34;Remounting via /etc/fstab\u0026#34; sudo mount -a step \u0026#34;Restarting Samba (smbd holds cached handles from before the remount)\u0026#34; sudo systemctl restart smb nmb msg \u0026#34;Samba restarted\u0026#34; step \u0026#34;Verifying each share\u0026#34; failed=0 for d in \u0026#34;$data_root\u0026#34;/*; do [[ -d \u0026#34;$d\u0026#34; ]] || continue name=$(basename \u0026#34;$d\u0026#34;) if ls \u0026#34;$d\u0026#34; \u0026amp;\u0026gt;/dev/null; then msg \u0026#34;$name — accessible\u0026#34; else err \u0026#34;$name — still not accessible\u0026#34; failed=1 fi done step \u0026#34;Result\u0026#34; if [[ \u0026#34;$failed\u0026#34; -eq 0 ]]; then msg \u0026#34;All shares recovered.\u0026#34; else warn \u0026#34;Some shares are still broken.\u0026#34; echo \u0026#34; This usually means the QNAP\u0026#39;s own NFS export table needs a reboot:\u0026#34; echo \u0026#34; ssh admin@${nas_ip}\u0026#34; echo \u0026#34; cat /proc/fs/nfsd/exports # if empty, that\u0026#39;s the problem\u0026#34; echo \u0026#34; Control Panel -\u0026gt; System -\u0026gt; Power -\u0026gt; Restart\u0026#34; fi sudo tee /etc/ssh/sshd_config.d/keepalive.conf \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; ClientAliveInterval 60 ClientAliveCountMax 3 EOF sudo systemctl restart sshd ","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/jacksparrow2-qnap-remount/","summary":"\u003ch2 id=\"same-symptom-completely-different-machine\"\u003eSame symptom, completely different machine\u003c/h2\u003e\n\u003cp\u003eI already wrote about the remount script I built for my NixOS desktop \u0026hellip;\nclearing stale network mount units through systemd when a share drops.\u003c/p\u003e\n\u003cp\u003eThis is the other half of that same story, except this one lives on\nJackSparrow2 itself (the T620 server) and fixes a genuinely different\nfailure, even though the symptom looks identical from the client side:\na share that used to work suddenly doesn\u0026rsquo;t.\u003c/p\u003e","title":"The Companion Script: Fixing Stale QNAP Mounts on the Server Side"},{"content":"The problem this actually solves On a normal distro, if a network mount goes stale \u0026hellip; the connection dropped, the server bounced, whatever \u0026hellip; the fix is usually sudo umount then sudo mount -a, reading straight from /etc/fstab. Simple, because fstab is a plain text file you can just point tools at.\nNixOS doesn\u0026rsquo;t work that way. fileSystems entries in configuration.nix get compiled into individual systemd .mount units automatically at build time. There\u0026rsquo;s no /etc/fstab to edit or run mount -a against in the traditional sense \u0026hellip; the mounts exist purely as generated systemd units with names like mnt-t620\\x2drelationships.mount.\nWhen one of those goes stale, the usual muscle-memory fix doesn\u0026rsquo;t apply, and I found that out the hard way the first time a share on my T620 dropped and I instinctively reached for commands that don\u0026rsquo;t do anything useful on this system.\nWhy not just reboot every time I could. Rebooting always clears stale mounts. But rebooting the whole desktop because one network share got confused is a heavy hammer for a small problem, and I wanted something I could run in five seconds without closing everything I had open.\nWhat the script actually does, step by step step \u0026#34;Stopping all network mount units\u0026#34; units=$(systemctl list-units --type=mount --all --no-legend | \\ awk \u0026#39;{print $1}\u0026#39; | grep -E \u0026#39;^mnt-(t620|nfs)\u0026#39;) Instead of hardcoding every mount name, it asks systemd directly for every currently-known mount unit and filters down to the ones that match my naming pattern \u0026hellip; mnt-t620-* for the Samba shares off my T620, mnt-nfs-* for the direct QNAP NFS mounts. This means if I add or remove a share in my NixOS config later, the script doesn\u0026rsquo;t need updating \u0026hellip; it just discovers whatever\u0026rsquo;s actually there at the time it runs.\nfor u in $units; do info \u0026#34;Stopping $u\u0026#34; sudo systemctl stop \u0026#34;$u\u0026#34; done Stop each one properly through systemd, not by force-unmounting first \u0026hellip; letting the unit shut itself down cleanly avoids leaving anything in a half-torn-down state.\nThe force-clear step, and why it\u0026rsquo;s there at all step \u0026#34;Force-clearing anything still lingering\u0026#34; for d in /mnt/t620-* /mnt/nfs-*; do [[ -d \u0026#34;$d\u0026#34; ]] || continue if mountpoint -q \u0026#34;$d\u0026#34;; then warn \u0026#34;$d still mounted, forcing unmount\u0026#34; sudo umount -f -l \u0026#34;$d\u0026#34; 2\u0026gt;\u0026amp;1 fi done This exists because I\u0026rsquo;ve genuinely seen a mount survive a clean systemctl stop and still show as mounted afterward \u0026hellip; usually when whatever\u0026rsquo;s on the other end (the T620, in my case) was slow to respond to the disconnect. Rather than assume the stop always worked, this checks every known mountpoint directly with mountpoint -q and only force-unmounts (-f -l, force plus lazy) the ones still actually showing as mounted. Doesn\u0026rsquo;t touch anything that already came down cleanly.\nGetting everything back step \u0026#34;Reloading systemd and remounting everything\u0026#34; sudo systemctl daemon-reload sudo systemctl restart remote-fs.target remote-fs.target is the systemd target that all network filesystem mounts hang off of \u0026hellip; restarting it tells systemd to bring every associated mount unit back up in the correct order, which is the closest NixOS equivalent to a plain mount -a on a system with a real fstab.\nThe verification steps, and why I didn\u0026rsquo;t just trust it worked systemctl --failed | grep -E \u0026#39;mnt-(t620|nfs)\u0026#39; \u0026amp;\u0026amp; warn \u0026#34;Some mounts still failed\u0026#34; \\ || msg \u0026#34;No failed network mount units\u0026#34; Checking systemctl --failed specifically for my mount pattern tells me immediately if something didn\u0026rsquo;t come back, rather than assuming success just because the script ran without an error exiting it.\nfor d in /mnt/t620-* /mnt/nfs-*; do [[ -d \u0026#34;$d\u0026#34; ]] || continue if ls \u0026#34;$d\u0026#34; \u0026amp;\u0026gt;/dev/null; then msg \u0026#34;$d ... accessible\u0026#34; else err \u0026#34;$d ... NOT accessible\u0026#34; fi done The real proof isn\u0026rsquo;t \u0026ldquo;systemd thinks the mount is active,\u0026rdquo; it\u0026rsquo;s \u0026ldquo;can I actually list files in it.\u0026rdquo; A mount unit can report as active while the underlying share is unresponsive \u0026hellip; this last check is the one that actually matters, and it\u0026rsquo;s deliberately the very last thing the script does.\nRunning it sudo ./remount-network-shares.sh Shows current mounts, tears down anything matching the pattern, brings it all back through systemd properly, then proves each mountpoint is genuinely reachable \u0026hellip; not just reported as mounted. Five seconds, no reboot, and I get real confirmation instead of a guess.\n#!/usr/bin/env bash # ============================================================================= # remount-network-shares.sh # Tolga Erok # Version : 1.0 # Date : 11 Jul 2026 # # Clears stale NFS/CIFS mounts on G4-NIXOS and remounts everything cleanly. # On NixOS, fileSystems entries in configuration.nix generate individual # systemd .mount units automatically -- my script targets those units # directly rather than touching /etc/fstab (there isn\u0026#39;t one to edit). # ============================================================================= clear set -uo pipefail red=\u0026#39;\\e[31m\u0026#39;; grn=\u0026#39;\\e[32m\u0026#39;; yel=\u0026#39;\\e[33m\u0026#39;; cyn=\u0026#39;\\e[36m\u0026#39;; rst=\u0026#39;\\e[0m\u0026#39;; bld=\u0026#39;\\e[1m\u0026#39; msg() { echo -e \u0026#34;${grn}${bld}[ OK ]${rst} $*\u0026#34;; } info() { echo -e \u0026#34;${cyn}${bld}[ INFO ]${rst} $*\u0026#34;; } warn() { echo -e \u0026#34;${yel}${bld}[ WARN ]${rst} $*\u0026#34;; } err() { echo -e \u0026#34;${red}${bld}[ FAIL ]${rst} $*\u0026#34;; } step() { echo -e \u0026#34;\\n${yel}${bld} ──► $*${rst}\u0026#34;; } step \u0026#34;Current network mounts\u0026#34; mount -t cifs,nfs,nfs4 step \u0026#34;Stopping all network mount units\u0026#34; units=$(systemctl list-units --type=mount --all --no-legend | \\ awk \u0026#39;{print $1}\u0026#39; | grep -E \u0026#39;^mnt-(t620|nfs)\u0026#39;) if [[ -z \u0026#34;$units\u0026#34; ]]; then warn \u0026#34;No matching mount units found -- check naming with: systemctl list-units --type=mount\u0026#34; else for u in $units; do info \u0026#34;Stopping $u\u0026#34; sudo systemctl stop \u0026#34;$u\u0026#34; done fi step \u0026#34;Force-clearing anything still lingering\u0026#34; for d in /mnt/t620-* /mnt/nfs-*; do [[ -d \u0026#34;$d\u0026#34; ]] || continue if mountpoint -q \u0026#34;$d\u0026#34;; then warn \u0026#34;$d still mounted, forcing unmount\u0026#34; sudo umount -f -l \u0026#34;$d\u0026#34; 2\u0026gt;\u0026amp;1 fi done step \u0026#34;Reloading systemd and remounting everything\u0026#34; sudo systemctl daemon-reload sudo systemctl restart remote-fs.target step \u0026#34;Verifying\u0026#34; sleep 2 mount -t cifs,nfs,nfs4 echo systemctl --failed | grep -E \u0026#39;mnt-(t620|nfs)\u0026#39; \u0026amp;\u0026amp; warn \u0026#34;Some mounts still failed -- check the unit above\u0026#34; \\ || msg \u0026#34;No failed network mount units\u0026#34; step \u0026#34;Result\u0026#34; for d in /mnt/t620-* /mnt/nfs-*; do [[ -d \u0026#34;$d\u0026#34; ]] || continue if ls \u0026#34;$d\u0026#34; \u0026amp;\u0026gt;/dev/null; then msg \u0026#34;$d — accessible\u0026#34; else err \u0026#34;$d — NOT accessible\u0026#34; fi done ","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/nixos-remount-network-shares/","summary":"\u003ch2 id=\"the-problem-this-actually-solves\"\u003eThe problem this actually solves\u003c/h2\u003e\n\u003cp\u003eOn a normal distro, if a network mount goes stale \u0026hellip; the connection\ndropped, the server bounced, whatever \u0026hellip; the fix is usually\n\u003ccode\u003esudo umount\u003c/code\u003e then \u003ccode\u003esudo mount -a\u003c/code\u003e, reading straight from \u003ccode\u003e/etc/fstab\u003c/code\u003e.\nSimple, because fstab is a plain text file you can just point tools at.\u003c/p\u003e\n\u003cp\u003eNixOS doesn\u0026rsquo;t work that way. \u003ccode\u003efileSystems\u003c/code\u003e entries in\n\u003ccode\u003econfiguration.nix\u003c/code\u003e get compiled into individual systemd \u003ccode\u003e.mount\u003c/code\u003e units\nautomatically at build time. There\u0026rsquo;s no \u003ccode\u003e/etc/fstab\u003c/code\u003e to edit or run\n\u003ccode\u003emount -a\u003c/code\u003e against in the traditional sense \u0026hellip; the mounts exist purely as\ngenerated systemd units with names like \u003ccode\u003emnt-t620\\x2drelationships.mount\u003c/code\u003e.\u003c/p\u003e","title":"Why I Wrote a Remount Script for My NixOS Network Shares"},{"content":"Why I bothered making this an alias at all Btrfs needs a bit more active maintenance than ext4 or XFS \u0026hellip; it\u0026rsquo;s got its own checksumming, its own way of allocating chunks across a volume, and if you never touch it, things can quietly degrade in ways you won\u0026rsquo;t notice until something actually breaks. I got tired of typing three separate commands in the right order every time, so I turned it into one alias that does all three, in sequence, with actual visible output so I know what stage it\u0026rsquo;s on.\nalias scrub=\u0026#39; sudo -v \u0026amp;\u0026amp; \\ echo -e \u0026#34;${YELLOW}\\n─── 🔍 Starting Btrfs Scrub... ───────────────────────────────────────────────${NC}\u0026#34; \u0026amp;\u0026amp; \\ sudo btrfs scrub start -B / \u0026amp;\u0026amp; echo \u0026#34;✅ Scrub done.\u0026#34; \u0026amp;\u0026amp; \\ echo -e \u0026#34;${YELLOW}\\n─── 🔄 Running Btrfs Balance (dusage=75, musage=75)... ─────────────────────────────${NC}\u0026#34; \u0026amp;\u0026amp; \\ sudo btrfs balance start -dusage=75 -musage=75 -v / \u0026amp;\u0026amp; echo \u0026#34;✅ Balance done.\u0026#34; \u0026amp;\u0026amp; \\ echo -e \u0026#34;${YELLOW}\\n─── ✂️ Trimming Filesystems... ─────────────────────────────────────────────────${NC}\u0026#34; \u0026amp;\u0026amp; \\ sudo fstrim -av \u0026amp;\u0026amp; echo \u0026#34;🚀 Trim completed.\u0026#34; \u0026#39; Breaking down each piece sudo -v \u0026hellip; asks for the sudo password once, up front, before any of the actual work starts. Everything after this in the chain uses the cached credential instead of prompting mid-scrub. Small thing, but it means the whole alias runs uninterrupted once you\u0026rsquo;ve typed your password, instead of stopping to ask again partway through a long scrub.\nbtrfs scrub start -B / \u0026hellip; this is the actual data integrity check. Btrfs reads every block, verifies its checksum, and automatically repairs anything that doesn\u0026rsquo;t match, using redundant copies if you\u0026rsquo;re on RAID1 or similar. -B runs it in the foreground instead of backgrounding it \u0026hellip; I want to actually watch it happen and know exactly when it\u0026rsquo;s finished, not have it silently running while I do something else and forget about it.\nbtrfs balance start -dusage=75 -musage=75 -v / \u0026hellip; this is the one most people skip, and the one that actually matters over time. Btrfs allocates storage in \u0026ldquo;chunks,\u0026rdquo; and as you delete and rewrite files, chunks can end up mostly empty but still allocated, wasting space and fragmenting your actual usable capacity.\n-dusage=75 tells balance to only touch data chunks that are 75% full or less \u0026hellip; meaning it consolidates the genuinely underused chunks and leaves the ones that are already efficiently packed alone. -musage=75 does the same thing for metadata chunks specifically.\nI picked 75 deliberately, not the more aggressive numbers some guides throw around. A full, unrestricted balance (btrfs balance start / with no usage filter) rewrites everything, which on a real filesystem with real data takes a genuinely long time and hammers the disk the whole way through. Filtering at 75% means it only touches chunks that actually need consolidating, finishes in a fraction of the time, and does exactly the useful part of the job without the wasted effort on chunks that were already fine.\n-v just gives verbose output \u0026hellip; I want to see it actually working, not stare at a blank terminal wondering if it\u0026rsquo;s frozen or just slow.\nfstrim -av \u0026hellip; last step, and the simplest. Tells the SSD which blocks are no longer in use so the drive\u0026rsquo;s own controller can erase them ahead of time, keeping write performance from degrading over the drive\u0026rsquo;s life. -a runs it against every mounted filesystem that supports it, not just root \u0026hellip; -v again just so I can see it actually did something rather than running silently.\nWhy in that specific order Scrub first, because there\u0026rsquo;s no point balancing or trimming a filesystem that might have silent corruption sitting in it \u0026hellip; fix data integrity before doing anything else. Balance second, because consolidating chunks before trimming means fstrim has a cleaner, more accurate picture of what\u0026rsquo;s actually free to trim, rather than trimming around a bunch of half-empty chunks that balance is about to rewrite anyway. Trim last, since it\u0026rsquo;s cheap and there\u0026rsquo;s no reason to do it before the more disruptive operations are done.\nRunning it scrub That\u0026rsquo;s it \u0026hellip; the whole point of wrapping it in an alias. One word, three maintenance jobs, correct order, and coloured section headers so I can tell at a glance which stage it\u0026rsquo;s on without reading closely.\nThe thing I\u0026rsquo;d tell anyone copying this Don\u0026rsquo;t blindly copy the 75 numbers without thinking about your own filesystem. On a mostly-static filesystem that doesn\u0026rsquo;t rewrite much data, a higher usage filter (say 90) means balance barely touches anything, since most chunks are already well-packed. On a filesystem that churns a lot \u0026hellip; lots of file creation and deletion \u0026hellip; a lower number does more consolidating work each run. 75 was the right middle ground for how I actually use my own machine, not a universal correct answer.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/btrfs-scrub-alias/","summary":"\u003ch2 id=\"why-i-bothered-making-this-an-alias-at-all\"\u003eWhy I bothered making this an alias at all\u003c/h2\u003e\n\u003cp\u003eBtrfs needs a bit more active maintenance than ext4 or XFS \u0026hellip; it\u0026rsquo;s got\nits own checksumming, its own way of allocating chunks across a volume,\nand if you never touch it, things can quietly degrade in ways you won\u0026rsquo;t\nnotice until something actually breaks. I got tired of typing three\nseparate commands in the right order every time, so I turned it into\none alias that does all three, in sequence, with actual visible output\nso I know what stage it\u0026rsquo;s on.\u003c/p\u003e","title":"My Btrfs Scrub Alias, and What Every Flag in It Actually Does"},{"content":"The hardware I built this on an HP T620 \u0026hellip; a thin client, not a real server. Dual-core AMD GX-217GA SoC, the kind of chip that used to sit in front of a cash register or a hospital terminal, not something anyone would call fast. No AES-NI either, which matters more than you\u0026rsquo;d think once you\u0026rsquo;re running encrypted traffic through it all day. If you\u0026rsquo;ve got something like this sitting in a drawer, it\u0026rsquo;s genuinely enough to run a small personal NAS. Don\u0026rsquo;t expect it to be quick. Expect it to just work, reliably, once it\u0026rsquo;s set up right.\nInstalling Fedora Server I used the Fedora Server \u0026ldquo;Everything\u0026rdquo; ISO rather than the regular netinstall \u0026hellip; it pulls a fuller package set from the start, which saved me chasing down individual packages later for things like samba, nfs-utils, cockpit, and firewalld that I knew I\u0026rsquo;d want anyway.\nStandard install, nothing exotic. I did make one deliberate choice at partitioning: XFS for root, not ext4. XFS handles the kind of mixed read/write traffic a file server generates a bit better, and its journal tuning options gave me a real, measurable improvement later once I actually had Samba/SFTP traffic flowing through it.\nThe login banner Small thing, did it early, still happy I did. Every time I SSH into this box, I get a real banner, not the generic Fedora default \u0026hellip; tells me immediately which machine I\u0026rsquo;m on and that I should be paying attention:\nsudo nano /etc/ssh/banner Paste whatever ASCII art or plain text you want \u0026hellip; mine\u0026rsquo;s a big block\n░░█ ▄▀█ █▀▀ █▄▀ █▀ █▀█ ▄▀█ █▀█ █▀█ █▀█ █░█░█ ▀█ █▄█ █▀█ █▄▄ █░█ ▄█ █▀▀ █▀█ █▀▄ █▀▄ █▄█ ▀▄▀▄▀ █▄ (ツ)_/¯ 🌱 ╔═══════════════════════════════════════════════════════╗ ║ JackSparrow2 — HP T620 NAS | Fedora Server 44 ║ ║ Authorised access only. All sessions are logged. ║ ╚═══════════════════════════════════════════════════════╝ 🍄 \u0026ldquo;JACKSPARROW2\u0026rdquo; header, a small note about what the box actually is, and a line making clear sessions are logged. Then point sshd at it:\nsudo nano /etc/ssh/sshd_config Add or edit:\nBanner /etc/ssh/banner sudo sshd -t \u0026amp;\u0026amp; sudo systemctl restart sshd sshd -t first \u0026hellip; validates the config before you actually restart the service, so a typo doesn\u0026rsquo;t lock you out of your own SSH session. Learned that habit the hard way on something else, kept it for everything since.\nSamba \u0026hellip; the actual file sharing Install it:\nsudo dnf install -y samba samba-client Here\u0026rsquo;s the /etc/samba/smb.conf global section I landed on, after getting bitten by leaving anonymous access open by default without realising it:\n[global] restrict anonymous = 2 access based share enum = yes workgroup = WORKGROUP server string = MyServer NAS security = user map to guest = never server role = standalone server passdb backend = tdbsam min protocol = SMB2 max protocol = SMB3 map to guest = never and restrict anonymous = 2 together mean nobody gets in without a real account and password \u0026hellip; no accidental open share reachable by anyone who finds the IP. I found this out by testing it myself with a fake username after the fact and being surprised it let me see a share I definitely didn\u0026rsquo;t mean to expose.\naccess based share enum = yes is the one I\u0026rsquo;d call genuinely essential if more than one person uses the box \u0026hellip; it hides shares from the browse list entirely for anyone who isn\u0026rsquo;t allowed to access them, instead of just denying the connection after they\u0026rsquo;ve already seen the name.\nPer-user home folders on the same server This is the part that took me longest to get right, and the part most guides skip. I wanted each user to log in and land in their own private space, automatically, without me manually creating a share per person.\nSamba has a built-in special share for exactly this:\n[homes] comment = Home Directories browseable = no writable = yes valid users = %S create mask = 0700 directory mask = 0700 %S is the trick \u0026hellip; it resolves to whatever share name is being requested, and for [homes] that\u0026rsquo;s automatically the connecting username. Brian connects, he lands in his own folder. Muriel connects, she lands in hers. Neither sees the other\u0026rsquo;s, and neither shows up in anyone else\u0026rsquo;s browse list.\nEach person still needs an actual account to log in with:\nsudo useradd -M -s /sbin/nologin brian sudo smbpasswd -a brian -M skips creating a real Linux home directory since Samba\u0026rsquo;s [homes] share handles that. -s /sbin/nologin means they get file access only \u0026hellip; no shell, no SSH login, nothing beyond what Samba gives them.\nSFTP \u0026hellip; the part I fought with the most I wanted SFTP too, not just Samba, so people could pull files without needing a full Samba client. The naive way is just enabling SFTP subsystem in sshd_config and calling it done \u0026hellip; except that gives anyone with an account full filesystem browsing, not just their own folder. Not what I wanted.\nThe actual fix is a chroot jail per user. Add a group:\nsudo groupadd sftpusers sudo usermod -aG sftpusers brian Then in sshd_config:\nMatch Group sftpusers ChrootDirectory /srv/sftp/%u ForceCommand internal-sftp X11Forwarding no AllowTcpForwarding no PermitTunnel no The one thing that cost me real time here: SSH refuses the entire session if the chroot directory itself isn\u0026rsquo;t owned root:root with 755 permissions \u0026hellip; not the content inside it, the jail root folder specifically. Get that wrong and you don\u0026rsquo;t get a helpful error, you just get disconnected with no obvious reason why.\nsudo mkdir -p /srv/sftp/brian/home sudo chown root:root /srv/sftp/brian sudo chmod 755 /srv/sftp/brian Their actual files live one level in, bind-mounted from their real home directory:\nsudo mount --bind /home/brian /srv/sftp/brian/home That bind mount needs to survive a reboot or it silently stops working and nobody notices until someone can\u0026rsquo;t log in. Add it properly to /etc/fstab:\n/home/brian /srv/sftp/brian/home none bind,nofail 0 0 What I\u0026rsquo;d tell someone trying this themselves Test the SSH banner and config with sshd -t before ever restarting the service \u0026hellip; don\u0026rsquo;t lock yourself out over a typo. Set map to guest = never on Samba from day one, don\u0026rsquo;t find out later that guest access was quietly open. And if you\u0026rsquo;re doing per-user SFTP jails for more than one or two people, don\u0026rsquo;t hand-write the bind mounts one at a time \u0026hellip; write one small script that loops through everyone and does it consistently, because you will forget the exact steps by the third person and start making small inconsistent mistakes.\nNone of this needed expensive hardware. It needed patience, a habit of actually reading the error messages instead of guessing, and a willingness to redo something three times until it was actually right, not just working by accident.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/build-your-own-nas/","summary":"\u003ch2 id=\"the-hardware\"\u003eThe hardware\u003c/h2\u003e\n\u003cp\u003eI built this on an HP T620 \u0026hellip; a thin client, not a real server. Dual-core\nAMD GX-217GA SoC, the kind of chip that used to sit in front of a cash\nregister or a hospital terminal, not something anyone would call fast.\nNo AES-NI either, which matters more than you\u0026rsquo;d think once you\u0026rsquo;re\nrunning encrypted traffic through it all day. If you\u0026rsquo;ve got something\nlike this sitting in a drawer, it\u0026rsquo;s genuinely enough to run a small\npersonal NAS. Don\u0026rsquo;t expect it to be quick. Expect it to just work,\nreliably, once it\u0026rsquo;s set up right.\u003c/p\u003e","title":"Building My Own NAS on an HP T620: Fedora Server, Samba, SFTP, and a Login Banner"},{"content":"Why negativo17 over RPM Fusion RPM Fusion\u0026rsquo;s NVIDIA packages work, but negativo17\u0026rsquo;s repo tends to track driver releases faster and packages them with fewer of the DKMS-related headaches that show up after a kernel update. For a desktop where I don\u0026rsquo;t want to babysit whether the driver rebuilt correctly after every dnf upgrade, that reliability matters more than which repo is more \u0026ldquo;official.\u0026rdquo;\nAdding the repos Two separate repos \u0026hellip; one for the driver itself, one for the multimedia codec stack that pairs with it:\nsudo dnf5 config-manager addrepo --from-repofile=https://negativo17.org/repos/fedora-nvidia.repo sudo dnf5 config-manager addrepo --from-repofile=https://negativo17.org/repos/fedora-multimedia.repo sudo dnf5 makecache Installing the actual driver stack sudo dnf5 install -y \\ nvidia-driver \\ nvidia-driver-cuda \\ nvidia-driver-cuda-libs \\ nvidia-settings \\ nvidia-driver-libs.i686 The .i686 package matters if you run anything 32-bit \u0026hellip; Steam, older games, some proprietary software \u0026hellip; that expects 32-bit NVIDIA libraries to actually be present, not just the 64-bit driver.\nRebuilding initramfs before rebooting This step gets skipped by people copying half a guide, and then they wonder why the driver \u0026ldquo;doesn\u0026rsquo;t load\u0026rdquo;:\nsudo dracut --force sudo reboot The kernel needs the NVIDIA modules baked into initramfs to load them at the right point in boot \u0026hellip; installing the package alone isn\u0026rsquo;t enough if initramfs hasn\u0026rsquo;t been regenerated to include it.\nConfirming it actually worked nvidia-smi Should show your card, driver version, and current utilisation \u0026hellip; not an error about no devices found.\nrpm -qa | grep nvidia-driver Confirms the actual installed package versions.\necho $XDG_SESSION_TYPE Should say wayland if you\u0026rsquo;re running Plasma on Wayland \u0026hellip; worth checking specifically after an NVIDIA driver install, since NVIDIA\u0026rsquo;s Wayland support has historically been the rougher edge compared to X11, and a session silently falling back to X11 after a driver change is a real thing that happens.\nls /etc/yum.repos.d/ | grep -i nvidia Confirms the repo itself actually registered correctly, useful if dnf ever complains it can\u0026rsquo;t find the driver packages on a later upgrade.\nThe actual result Driver installed, boots clean, nvidia-smi reports correctly, Wayland session confirmed still active rather than silently downgrading to X11. No DKMS rebuild drama on the next kernel update, which was the whole point of picking negativo17 over the alternative in the first place.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/nvidia-negativo17/","summary":"\u003ch2 id=\"why-negativo17-over-rpm-fusion\"\u003eWhy negativo17 over RPM Fusion\u003c/h2\u003e\n\u003cp\u003eRPM Fusion\u0026rsquo;s NVIDIA packages work, but negativo17\u0026rsquo;s repo tends to track\ndriver releases faster and packages them with fewer of the DKMS-related\nheadaches that show up after a kernel update. For a desktop where I\ndon\u0026rsquo;t want to babysit whether the driver rebuilt correctly after every\n\u003ccode\u003ednf upgrade\u003c/code\u003e, that reliability matters more than which repo is more\n\u0026ldquo;official.\u0026rdquo;\u003c/p\u003e\n\u003ch2 id=\"adding-the-repos\"\u003eAdding the repos\u003c/h2\u003e\n\u003cp\u003eTwo separate repos \u0026hellip; one for the driver itself, one for the multimedia\ncodec stack that pairs with it:\u003c/p\u003e","title":"NVIDIA Drivers on Fedora the negativo17 Way"},{"content":"The actual bug WPS Office\u0026rsquo;s flatpak ships a background helper called wpscloudsvr that reliably segfaults \u0026hellip; null-pointer deref inside libqingbangong.so, r15=0x0, the works. Not a config issue, not something I broke, just a genuinely broken binary shipped in the package. It doesn\u0026rsquo;t crash the whole app, just spams crash reports and burns CPU respawning itself.\nThe fix nobody\u0026rsquo;s going to like, but it works You can\u0026rsquo;t easily patch a binary inside a flatpak sandbox, and there\u0026rsquo;s no config flag to just disable this one helper. So: bind-mount /dev/null directly over the broken executable. The file still \u0026ldquo;exists\u0026rdquo; as far as the OS is concerned, it just contains nothing \u0026hellip; attempting to execute it does nothing, silently, forever.\nmount --bind /dev/null \u0026#34;$CURRENT_TARGET\u0026#34; Crude. Also completely effective, and unlike trying to chmod -x it or delete it, this survives the app trying to repair itself, because the flatpak runtime still sees a file sitting exactly where it expects one.\nThe part that actually took the effort: making it stick A one-off bind mount doesn\u0026rsquo;t survive a reboot, and flatpak updates regularly move the actual binary to a new content-hash directory, which would silently un-mask it the moment WPS updates. Neither of those is acceptable for something you want to just stay fixed.\nResolving the path dynamically instead of hardcoding it:\nAPP_ID=\u0026#34;com.wps.Office\u0026#34; TARGET_REL=\u0026#34;extra/wps-office/office6/wpscloudsvr\u0026#34; DEPLOY_DIR=$(flatpak info --show-location \u0026#34;$APP_ID\u0026#34;) CURRENT_TARGET=\u0026#34;$DEPLOY_DIR/files/$TARGET_REL\u0026#34; flatpak info --show-location always returns wherever the current deployment actually lives, regardless of which content-hash directory that happens to be today. That\u0026rsquo;s the whole trick \u0026hellip; never hardcode the hash, always ask flatpak where it currently thinks the app is.\nIdempotent (big word! Lol), so it\u0026rsquo;s safe to call repeatedly without erroring:\nif mountpoint -q \u0026#34;$CURRENT_TARGET\u0026#34;; then echo \u0026#34;Already masked at: $CURRENT_TARGET\u0026#34; exit 0 fi Two systemd units cover the two ways this needs to reapply itself \u0026hellip; once at boot (since bind mounts don\u0026rsquo;t survive a restart), and once live, any time flatpak actually updates the app and moves the binary to a new hash path:\n# wps-cloudsvr-mask-boot.service [Unit] Description=LinuxTweaks Mask wpscloudsvr on boot After=local-fs.target [Service] Type=oneshot ExecStart=/usr/local/bin/mask-wps-cloudsvr.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target # wps-cloudsvr-mask.path [Unit] Description=LinuxTweaks Watch for WPS flatpak updates and re-mask [Path] PathModified=/var/lib/flatpak/app/com.wps.Office Unit=wps-cloudsvr-mask.service [Install] WantedBy=multi-user.target The .path unit watches the flatpak install directory itself \u0026hellip; the moment an update touches it, it fires the mask service again, which resolves the new hash path and reapplies the mount. No manual intervention needed after a WPS update, ever.\nInstalling it sudo ./install.sh Copies the script to /usr/local/bin/, installs both units, enables them, and runs the mask once immediately to confirm it works \u0026hellip; no waiting for the next boot or next update to see if it\u0026rsquo;s actually applied.\nsystemctl status wps-cloudsvr-mask.path wps-cloudsvr-mask-boot.service mount | grep wpscloudsvr The second command is the real proof \u0026hellip; you should see /dev/null bind-mounted over the actual binary path.\nWhy bother with this much ceremony for one broken helper Because the alternative was periodically noticing the crash reports again after every WPS update and re-doing this by hand. Once was tolerable. Solving it once, permanently, in a way that survives updates on its own, was worth the extra half hour of systemd unit writing.\nDownload the scripts as a ready-to-run bundle: LinuxTweaks-WPS-Masker.zip\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/wps-cloudsvr-crash-fix/","summary":"\u003ch2 id=\"the-actual-bug\"\u003eThe actual bug\u003c/h2\u003e\n\u003cp\u003eWPS Office\u0026rsquo;s flatpak ships a background helper called \u003ccode\u003ewpscloudsvr\u003c/code\u003e\nthat reliably segfaults \u0026hellip; null-pointer deref inside\n\u003ccode\u003elibqingbangong.so\u003c/code\u003e, \u003ccode\u003er15=0x0\u003c/code\u003e, the works. Not a config issue, not\nsomething I broke, just a genuinely broken binary shipped in the\npackage. It doesn\u0026rsquo;t crash the whole app, just spams crash reports and\nburns CPU respawning itself.\u003c/p\u003e\n\u003ch2 id=\"the-fix-nobodys-going-to-like-but-it-works\"\u003eThe fix nobody\u0026rsquo;s going to like, but it works\u003c/h2\u003e\n\u003cp\u003eYou can\u0026rsquo;t easily patch a binary inside a flatpak sandbox, and there\u0026rsquo;s\nno config flag to just disable this one helper. So: bind-mount\n\u003ccode\u003e/dev/null\u003c/code\u003e directly over the broken executable. The file still\n\u0026ldquo;exists\u0026rdquo; as far as the OS is concerned, it just contains nothing \u0026hellip;\nattempting to execute it does nothing, silently, forever.\u003c/p\u003e","title":"Killing a WPS Office Segfault the Ugly but Permanent Way"},{"content":"The advice everyone repeats without checking it Every \u0026ldquo;speed up your Linux install\u0026rdquo; guide tells you to slap noatime on every mount in /etc/fstab and call it a day. It\u0026rsquo;s not wrong exactly, but it\u0026rsquo;s not universally the win people treat it as either, and blindly copying it cost me nothing to try but also gave me nothing to gain on one of my own filesystems, for a reason worth actually understanding.\nnoatime vs relatime \u0026hellip; check your filesystem first atime tracking means every single file read also triggers a write to update the file\u0026rsquo;s last-accessed timestamp. On a filesystem that implements this naively, that\u0026rsquo;s a real write amplification problem \u0026hellip; noatime disables it completely and is a genuine, measurable win.\nExcept XFS hasn\u0026rsquo;t behaved that naively since 2006. It defaults to relatime \u0026hellip; only updates atime once a day, or if the file was modified since the last atime update, whichever comes first. The Arch Wiki puts it plainly: nobody really needs to bother with noatime on XFS specifically, because relatime\u0026rsquo;s default behaviour already captures essentially all of the benefit.\nSo on my XFS root and data volumes, I didn\u0026rsquo;t add noatime at all \u0026hellip; I left the default relatime alone, because there was no measurable win sitting on the table to grab. Worth checking what filesystem you\u0026rsquo;re actually on before copy-pasting noatime everywhere out of habit.\nWhat I did add instead, and why The two changes that actually did something on XFS specifically:\nUUID=xxxx / xfs defaults,logbufs=8,logbsize=256k,inode64 0 0 logbufs=8 and logbsize=256k increase the number and size of the XFS journal\u0026rsquo;s log buffers, which reduces how often the filesystem has to flush its internal journal for mixed read/write traffic \u0026hellip; relevant because this box is a NAS taking Samba/NFS traffic constantly, not sitting idle between big sequential writes.\ninode64 lets XFS allocate inodes across the whole filesystem instead of restricting them to the first terabyte. Doesn\u0026rsquo;t matter on a small drive. Matters the moment you\u0026rsquo;re planning to grow the volume later, and costs nothing to set now rather than discover the restriction after you\u0026rsquo;ve already filled the first terabyte.\ncommit \u0026hellip; the one I deliberately didn\u0026rsquo;t touch commit=N on ext4 (and similar knobs elsewhere) controls how often dirty data actually gets flushed to disk, in seconds. Raising it from the default trades a small amount of crash-safety for less disk activity, since more writes get batched together before an actual sync.\nI left this alone everywhere. It\u0026rsquo;s tempting on a spinning HDD specifically, where fewer, larger writes genuinely reduce mechanical wear and improve throughput. But the actual risk being traded away is real \u0026hellip; a longer commit interval means more data sitting in memory, unflushed, at the exact moment of a power loss or crash. For a NAS holding other people\u0026rsquo;s files, not just my own scratch data, that\u0026rsquo;s not a trade I want to make quietly by following a generic tuning guide. Default commit interval stays default.\nWhere SSD and NVMe actually change the calculus None of the above changes based on drive type \u0026hellip; XFS\u0026rsquo;s journal tuning and relatime behaviour don\u0026rsquo;t care whether they\u0026rsquo;re sitting on spinning rust or flash. What does change is trim behaviour, and this is the one place drive type genuinely dictates the right answer: discard mount option: NOT recommended\nXFS\u0026rsquo;s own documentation is explicit about this \u0026hellip; continuous discard on every delete has a real, measurable performance penalty, and the recommended approach is a periodic fstrim instead:\nsudo systemctl enable --now fstrim.timer Weekly by default, runs fstrim on every eligible mounted filesystem. On a spinning HDD this does nothing useful (no trim to speak of) and costs nothing to leave enabled. On SSD/NVMe it\u0026rsquo;s the actual right way to keep write performance from degrading over the drive\u0026rsquo;s life, without the per-operation overhead of continuous discard.\nnouuid \u0026hellip; the option nobody mentions until it bites you Not in any generic tuning guide, but real enough that it\u0026rsquo;s worth including here. If a drive ever gets physically moved to a different port or gets a fresh cable, XFS can sometimes flag it as having a \u0026ldquo;duplicate UUID\u0026rdquo; and refuse to mount \u0026hellip; genuinely the same filesystem, just XFS being cautious about a UUID collision after an unclean disconnect:\nUUID=xxxx /mnt/data2 xfs defaults,logbufs=8,logbsize=256k,inode64,nouuid 0 0 nouuid tells XFS to trust it\u0026rsquo;s the same filesystem rather than refusing out of caution. Doesn\u0026rsquo;t matter until the exact moment it does, and by then it\u0026rsquo;s an outage, not a tuning exercise.\nThe actual takeaway None of this is \u0026ldquo;here\u0026rsquo;s the maximally aggressive config, apply everywhere.\u0026rdquo; It\u0026rsquo;s closer to: know what your filesystem already handles well by default (XFS + atime), know what genuinely differs by drive type (trim behaviour), and know which knobs trade real safety for a marginal win that isn\u0026rsquo;t worth it on a box other people\u0026rsquo;s data lives on (commit interval). The generic advice isn\u0026rsquo;t wrong, it\u0026rsquo;s just generic \u0026hellip; the actual right config depends on reading what your specific filesystem already does before adding anything on top of it.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/fstab-tuning/","summary":"\u003ch2 id=\"the-advice-everyone-repeats-without-checking-it\"\u003eThe advice everyone repeats without checking it\u003c/h2\u003e\n\u003cp\u003eEvery \u0026ldquo;speed up your Linux install\u0026rdquo; guide tells you to slap \u003ccode\u003enoatime\u003c/code\u003e on\nevery mount in \u003ccode\u003e/etc/fstab\u003c/code\u003e and call it a day. It\u0026rsquo;s not wrong exactly,\nbut it\u0026rsquo;s not universally the win people treat it as either, and blindly\ncopying it cost me nothing to try but also gave me nothing to gain on\none of my own filesystems, for a reason worth actually understanding.\u003c/p\u003e","title":"fstab Tuning: noatime, commit, and Why I Chose What I Chose"},{"content":"The same sysctl file, three different machines I run the same core network tuning across the NAS (Fedora Server), the desktop (NixOS), and a laptop or two \u0026hellip; not because every machine has the same workload, but because the underlying kernel-level wins are the same regardless of what\u0026rsquo;s actually running on top. The differences show up in what else gets layered on, not in the base config.\nBBR + CAKE, together The single biggest win of the lot. tcp_congestion_control defaults to cubic on most distros, which is fine but not great, especially over Tailscale/WireGuard tunnels where the effective path characteristics don\u0026rsquo;t always match what cubic assumes. bbr measures actual bandwidth and round-trip time instead of just reacting to packet loss, and pairs specifically well with cake as the queuing discipline \u0026hellip; cake handles bufferbloat and fair queuing far better than the kernel\u0026rsquo;s default fq or pfifo_fast.\nnet.core.default_qdisc = cake net.ipv4.tcp_congestion_control = bbr Applied identically everywhere. This one isn\u0026rsquo;t workload-specific \u0026hellip; every machine benefits from better congestion control and less bufferbloat, no exceptions.\nCAKE needs an actual number, not just a name Here\u0026rsquo;s the bit I missed at first. Setting default_qdisc = cake in sysctl only makes CAKE the default \u0026hellip; it doesn\u0026rsquo;t actually configure any bandwidth shaping on its own. Without a real bandwidth figure, CAKE is running mostly blind on the exact thing it\u0026rsquo;s supposed to be best at, which is fighting bufferbloat.\nThe actual fix is setting it explicitly on the interface, with a number close to real-world throughput, not the theoretical link speed:\nsudo tc qdisc replace dev enp2s0 root cake bandwidth 900mbit Gigabit link, but realistically closer to 940mbit under ideal conditions \u0026hellip; 900mbit gives CAKE a number to actually manage queue depth against instead of guessing.\ntc commands don\u0026rsquo;t survive a reboot by themselves, so it needs a small service to reapply it at boot:\nsudo tee /etc/systemd/system/cake-shaping.service \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; [Unit] Description=Apply my CAKE bandwidth shaping After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/sbin/tc qdisc replace dev enp2s0 root cake bandwidth 900mbit RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now cake-shaping.service Confirm it\u0026rsquo;s actually doing something, not just assigned:\ntc -s qdisc show dev enp2s0 Real stats show up there \u0026hellip; drops, marks, backlog — if there\u0026rsquo;s ever congestion to actually manage.\nNetwork buffers, sized for the actual pipe Default TCP buffer sizes are conservative, tuned for a much wider range of hardware than what any single machine actually has. Since I\u0026rsquo;m mostly moving real file traffic \u0026hellip; Samba, NFS, SFTP \u0026hellip; over gigabit LAN and Tailscale, giving the kernel room to actually use the available bandwidth matters:\nnet.core.rmem_max = 134217728 net.core.wmem_max = 134217728 net.core.rmem_default = 262144 net.core.wmem_default = 262144 net.ipv4.tcp_rmem = 4096 87380 134217728 net.ipv4.tcp_wmem = 4096 65536 134217728 net.core.netdev_max_backlog = 5000 net.core.somaxconn = 1024 Same values across all three machines. The ceiling doesn\u0026rsquo;t hurt anything on a weaker box \u0026hellip; it\u0026rsquo;s a maximum, not a floor, the kernel still scales buffers dynamically underneath that.\nMTU black-hole avoidance This one\u0026rsquo;s specifically relevant if you\u0026rsquo;re running anything over Tailscale or another VPN/tunnel \u0026hellip; some network paths silently drop the ICMP messages that normal path MTU discovery depends on, which causes large transfers to hang intermittently for no obvious reason. It\u0026rsquo;s off by default in the kernel, and turning it on doesn\u0026rsquo;t cost anything unless a black hole actually gets detected, at which point it kicks in automatically:\nnet.ipv4.tcp_mtu_probing = 1 Cheap insurance. I added this specifically after digging into some inconsistent transfer behaviour over Tailscale and realising it was exactly the kind of thing this setting exists to catch.\nWhere the NAS-specific tuning splits off Everything above is universal. The NAS gets a few extra lines the desktop and laptop don\u0026rsquo;t need, because it\u0026rsquo;s actually serving NFS traffic instead of just consuming it:\nsunrpc.tcp_slot_table_entries = 128 sunrpc.udp_slot_table_entries = 128 These tune NFS client-side performance specifically \u0026hellip; relevant on the NAS because it\u0026rsquo;s mounting shares from another NAS behind it, and on any desktop that mounts NFS shares from the NAS itself. Doesn\u0026rsquo;t do anything on a machine that never touches NFS.\nMemory tuning \u0026hellip; same logic, different numbers per machine vm.swappiness and friends aren\u0026rsquo;t really \u0026ldquo;networking\u0026rdquo; but they live in the same sysctl file, and the values genuinely differ by machine, on purpose:\nvm.swappiness = 10 vm.dirty_ratio = 15 vm.dirty_background_ratio = 5 vm.vfs_cache_pressure = 50 On the file server (T620, 12GB RAM, no interactive session to protect), low swappiness just means the kernel doesn\u0026rsquo;t reach for swap until it genuinely has to, which is exactly what you want. The QNAP behind it doesn\u0026rsquo;t get any of this tuning at all \u0026hellip; it\u0026rsquo;s just the storage backend running its own QTS firmware, nothing here applies to it. Lower-RAM machines (an old laptop with 8GB, say) get more conservative settings across the board \u0026hellip; this isn\u0026rsquo;t a copy-paste-everywhere file, the RAM ceiling of each box changes what\u0026rsquo;s actually safe to set aggressively.\nThe one thing I don\u0026rsquo;t copy-paste blindly Socket options at the Samba level are a separate, related trap worth mentioning here since it looks similar but isn\u0026rsquo;t sysctl at all. I used to hardcode SO_RCVBUF/SO_SNDBUF values in smb.conf, thinking I was helping. Turns out fixed buffer sizes there actively override the kernel\u0026rsquo;s own auto-tuning \u0026hellip; the same tcp_rmem/tcp_wmem scaling described above \u0026hellip; and can throttle a fast, low-latency connection that would\u0026rsquo;ve done better left alone:\nsocket options = TCP_NODELAY Just that. No manual buffer sizes. Let the sysctl tuning above do the actual work, and Samba just gets out of the way.\nApplying it sudo tee /etc/sysctl.d/99-network-tuning.conf \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; net.core.default_qdisc = cake net.ipv4.tcp_congestion_control = bbr net.core.rmem_max = 134217728 net.core.wmem_max = 134217728 net.core.rmem_default = 262144 net.core.wmem_default = 262144 net.ipv4.tcp_rmem = 4096 87380 134217728 net.ipv4.tcp_wmem = 4096 65536 134217728 net.core.netdev_max_backlog = 5000 net.core.somaxconn = 1024 net.ipv4.tcp_mtu_probing = 1 EOF sudo sysctl --system Verify it actually took, since sysctl silently ignoring a typo is a real way to think something\u0026rsquo;s tuned when it isn\u0026rsquo;t:\nsysctl net.ipv4.tcp_congestion_control sysctl net.core.default_qdisc sysctl net.ipv4.tcp_mtu_probing Same base file, three machines, small deliberate differences where the workload actually calls for it \u0026hellip; that\u0026rsquo;s the whole approach.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/network-tuning/","summary":"\u003ch2 id=\"the-same-sysctl-file-three-different-machines\"\u003eThe same sysctl file, three different machines\u003c/h2\u003e\n\u003cp\u003eI run the same core network tuning across the NAS (Fedora Server), the\ndesktop (NixOS), and a laptop or two \u0026hellip; not because every machine has the\nsame workload, but because the underlying kernel-level wins are the same\nregardless of what\u0026rsquo;s actually running on top. The differences show up in\nwhat else gets layered on, not in the base config.\u003c/p\u003e\n\u003ch2 id=\"bbr--cake-together\"\u003eBBR + CAKE, together\u003c/h2\u003e\n\u003cp\u003eThe single biggest win of the lot. \u003ccode\u003etcp_congestion_control\u003c/code\u003e defaults to\n\u003ccode\u003ecubic\u003c/code\u003e on most distros, which is fine but not great, especially over\nTailscale/WireGuard tunnels where the effective path characteristics\ndon\u0026rsquo;t always match what \u003ccode\u003ecubic\u003c/code\u003e assumes. \u003ccode\u003ebbr\u003c/code\u003e measures actual bandwidth\nand round-trip time instead of just reacting to packet loss, and pairs\nspecifically well with \u003ccode\u003ecake\u003c/code\u003e as the queuing discipline \u0026hellip; \u003ccode\u003ecake\u003c/code\u003e handles\nbufferbloat and fair queuing far better than the kernel\u0026rsquo;s default \u003ccode\u003efq\u003c/code\u003e or\n\u003ccode\u003epfifo_fast\u003c/code\u003e.\u003c/p\u003e","title":"Networking Tweaks That Actually Stuck: CAKE, BBR, MTU Probing"},{"content":"Why this even matters Every block device on Linux has an I/O scheduler sitting between your applications and the actual disk, deciding the order requests get sent in. Get the wrong one for your workload and you\u0026rsquo;re leaving real performance on the table, or worse, adding latency you didn\u0026rsquo;t need to.\nCheck what\u0026rsquo;s currently active and what else is available:\ncat /sys/block/sda/queue/scheduler The one in brackets is active. On modern kernels you\u0026rsquo;ll usually see some combination of none, mq-deadline, kyber, and bfq listed.\nnone Does exactly what it says \u0026hellip; no reordering, no fairness logic, requests go straight to the device in the order they arrive. Sounds dumb until you realise that\u0026rsquo;s actually correct for very fast NVMe SSDs, which already handle massive internal queue depths and parallelism in hardware. Any scheduling logic on top just adds CPU overhead for no benefit. If you\u0026rsquo;re on a genuinely fast NVMe drive, none is often the right call, not a lack of configuration.\nmq-deadline The one I ended up on for my NAS. Deadline-based \u0026hellip; every request gets a deadline, and the scheduler makes sure nothing waits forever, while still batching reads and writes efficiently for decent throughput on traditional storage. Low overhead, predictable behaviour, and it doesn\u0026rsquo;t try to be clever about latency-fairness the way bfq does.\nFor a file server pushing Samba/NFS traffic with a weak CPU behind it, that low overhead matters more than anything else. Set it with a udev rule so it survives reboots without having to remember to reapply it:\nsudo tee /etc/udev/rules.d/60-ioscheduler.rules \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; ACTION==\u0026#34;add|change\u0026#34;, KERNEL==\u0026#34;sda\u0026#34;, ATTR{queue/scheduler}=\u0026#34;mq-deadline\u0026#34; EOF sudo udevadm control --reload-rules sudo udevadm trigger --subsystem-match=block kyber Built by Google originally for their own datacenter SSDs. Tuned to hit specific target latencies on fast, multi-queue flash storage under genuinely mixed random I/O \u0026hellip; think database or VM host workloads where you care about consistent response time under load, not raw sequential throughput.\nI tried it briefly, expecting it to be a straightforward upgrade over mq-deadline since it\u0026rsquo;s newer. It wasn\u0026rsquo;t, for my case specifically. My drive isn\u0026rsquo;t fast or parallel enough to actually benefit from what kyber is tuned for, and the scheduling logic itself costs more CPU than mq-deadline does \u0026hellip; on a weak APU with no crypto acceleration, that\u0026rsquo;s not a trade worth making for a NAS that\u0026rsquo;s mostly doing large sequential transfers, not latency-sensitive random access.\nbfq The default on a lot of desktop-oriented distros, including Fedora out of the box. Budget Fair Queuing \u0026hellip; genuinely good at what it\u0026rsquo;s built for, which is keeping the system responsive when multiple things are competing for disk access at once, classic desktop scenario: you\u0026rsquo;re copying a big file while also launching an app and don\u0026rsquo;t want the whole system to stutter.\nThat fairness logic isn\u0026rsquo;t free though. It\u0026rsquo;s the heaviest of the four in terms of CPU overhead, doing real work to keep everything balanced. Fine on a desktop CPU with cycles to spare. Not fine on a box where the CPU is already the bottleneck and there\u0026rsquo;s no interactive desktop session to protect in the first place \u0026hellip; a NAS doesn\u0026rsquo;t care about \u0026ldquo;responsiveness\u0026rdquo; the way a desktop does, it cares about throughput.\nWhat I actually landed on, and why mq-deadline, no contest, for this specific box:\nSATA-attached storage, not NVMe, so none gives up too much\nweak CPU, no AES-NI, already busy with Samba/NFS/SFTP/Tailscale \u0026hellip; so kyber\u0026rsquo;s and bfq\u0026rsquo;s extra scheduling overhead is a real cost, not a rounding error\nNAS workload is throughput-oriented sequential transfers, not the latency-sensitive random I/O kyber is actually built for\nno desktop session to protect, so bfq\u0026rsquo;s fairness logic is solving a problem that doesn\u0026rsquo;t exist on this machine\nIf this were a fast NVMe drive: none. If it were a database or VM host on fast flash: kyber. If it were a desktop I actually sit at: probably bfq, the default\u0026rsquo;s the default for a reason there. None of these are universally \u0026ldquo;the best\u0026rdquo; scheduler \u0026hellip; they\u0026rsquo;re each solving a different problem, and the only real mistake is picking one without knowing which problem you actually have.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/io-schedulers/","summary":"\u003ch2 id=\"why-this-even-matters\"\u003eWhy this even matters\u003c/h2\u003e\n\u003cp\u003eEvery block device on Linux has an I/O scheduler sitting between your\napplications and the actual disk, deciding the order requests get sent\nin. Get the wrong one for your workload and you\u0026rsquo;re leaving real\nperformance on the table, or worse, adding latency you didn\u0026rsquo;t need to.\u003c/p\u003e\n\u003cp\u003eCheck what\u0026rsquo;s currently active and what else is available:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecat /sys/block/sda/queue/scheduler\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe one in brackets is active. On modern kernels you\u0026rsquo;ll usually see some\ncombination of \u003ccode\u003enone\u003c/code\u003e, \u003ccode\u003emq-deadline\u003c/code\u003e, \u003ccode\u003ekyber\u003c/code\u003e, and \u003ccode\u003ebfq\u003c/code\u003e listed.\u003c/p\u003e","title":"I/O Schedulers: What They Actually Do, and Which One I Landed On"},{"content":"The idea was simple All I wanted was for my T620 to mount the QNAP shares, retire the QNAP as the \u0026ldquo;brain,\u0026rdquo; and have the T620 do everything \u0026hellip; Samba, SFTP, the lot \u0026hellip; while the QNAP just sat there as dumb storage in the background. Simple networking. I have never been more wrong about something taking one evening.\nBoot failure #1: the NFS ordering cycle First attempt, I set up NFS mounts from the QNAP onto the T620, then re-exported those same paths back out via NFS so other machines could reach them through the T620. Seemed logical \u0026hellip; one machine, one point of access.\nRebooted. Box wouldn\u0026rsquo;t come up. Emergency mode, root mounted read-only, the whole nightmare. Turns out re-exporting an NFS-mounted path via NFS again is a genuine, documented bug in nfs-utils/systemd \u0026hellip; not a Fedora thing, not something I did wrong, just a real contradiction in how nfs-server-generator builds its ordering dependencies. The mount wants to start before the server, the server wants to start before the mount, and systemd just gives up.\nFixed it by SSHing in, remounting root read-write, ripping out the bad config. Rebooted again. Same thing happened again a day later because the underlying fstab options (x-systemd.automount mixed with re-export) were still fighting each other.\nThe actual fix: stop trying to be clever. QNAP mounts get pulled onto the T620 via plain NFS with soft,timeo=30,retrans=3 \u0026hellip; no automount, no hard, nothing fancy. Anything that needs to go back out to other machines goes out via Samba, never NFS-over-NFS again. Sounds obvious now. Took two boot failures to get there.\nsudo tee -a /etc/fstab \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; 192.168.0.xxx:/Public /mnt/data/Public nfs _netdev,nfsvers=3,soft,timeo=30,retrans=3 0 0 EOF sudo systemctl daemon-reload sudo mount -a The permissions rabbit hole Once the mounts were stable, I hit an access problem that made no sense on paper. Some files in one of my shares just would not open \u0026hellip; ACL said I had full access, getfacl showed rwx right there in black and white, and Samba still handed back access denied.\nTurns out the QNAP\u0026rsquo;s NFSv3 server was enforcing raw UID matching underneath the ACL, and completely ignoring what the ACL said for any UID that wasn\u0026rsquo;t the file\u0026rsquo;s original owner. My Linux UID (1000) didn\u0026rsquo;t match the QNAP-side UID (502) that actually owned those files, and no amount of setfacl fixed it because NFSv3 doesn\u0026rsquo;t do username mapping \u0026hellip; just raw numbers, and QTS wasn\u0026rsquo;t respecting the ACL grant for a non-owning UID.\nThe fix: create a dummy local account with that exact UID (502), and tell Samba to force user = qnap-relationships on that specific share. Now Samba always accesses those files as the UID the QNAP actually trusts, no matter who\u0026rsquo;s really connected. Not elegant, but it works, and now I know to check ls -la for a raw number instead of a name \u0026hellip; that\u0026rsquo;s the tell that a UID isn\u0026rsquo;t mapping.\nsudo useradd -u 502 -M -s /sbin/nologin -g users qnap-relationships In smb.conf, under the affected share:\n[RELATIONSHIPS] force user = qnap-relationships sudo systemctl reload smb The HDD that ate my shares Added a second local drive for shared storage. Mounted it straight onto /mnt/data \u0026hellip; the same parent directory the QNAP shares were nested under. Instant chaos. Every share came back blank, Samba started throwing canonicalize_connect_path failed, and for about twenty horrible minutes I thought I\u0026rsquo;d lost data.\nNothing was lost. The new disk was just shadowing the QNAP submounts \u0026hellip; mount something on top of a directory, and whatever was already mounted underneath temporarily disappears from view. Classic Linux mount behaviour, obvious in hindsight, invisible in the moment when you\u0026rsquo;re staring at empty folders at 11pm.\nThe fix: local disks get their own completely separate mountpoint. Always. /mnt/data2, not anywhere near /mnt/data. Never again.\nsudo mkdir -p /mnt/data2 sudo tee -a /etc/fstab \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; UUID=xxxxxxxxxxxxxxxxxxxxxxx /mnt/data2 xfs defaults,nouuid 0 0 EOF sudo mount -a (nouuid also fixes a related \u0026ldquo;duplicate UUID\u0026rdquo; mount refusal if the same disk ever gets reconnected via a different cable or port.)\nSELinux, twice Got bit by SELinux context mismatches more than once. First time, a directory needed to serve both Samba and SFTP, and the default samba_share_t context only satisfied one of those \u0026hellip; SFTP kept getting silently denied until I relabeled it to public_content_rw_t, which happily serves both. Second time, same story with the Hugo web root for this very blog \u0026hellip; nginx got a flat 403 until I ran the same relabel pattern.\nNow it\u0026rsquo;s muscle memory: anything new that needs to be readable across multiple services gets checked with ls -Z before I even bother debugging further.\nsudo semanage fcontext -a -t public_content_rw_t \u0026#34;/mnt/data2/Public(/.*)?\u0026#34; sudo restorecon -Rv /mnt/data2/Public SFTP jails and the \u0026ldquo;forgot to persist it\u0026rdquo; trap Built proper SFTP chroot jails so each user gets dropped into just their own home folder plus a shared Public folder, nothing else visible. Worked great \u0026hellip; until a reboot, at which point every user\u0026rsquo;s SFTP access to Public just vanished with no error, nothing in the logs pointing at why.\nTurned out the bind mounts that made the jails work were only ever done live with mount --bind, never actually written anywhere permanent. A clean reboot means a clean slate, and nothing was there to redo the binding.\nThe fix, eventually: stopped hand-writing fstab lines per user entirely \u0026hellip; at four users it was already unmanageable, and I was thinking about a hundred. Wrote one small systemd service that scans /srv/sftp/* at boot and binds whatever it finds, automatically. Add a new user, their jail just works next boot. Zero fstab edits, ever again.\nsudo tee /usr/local/bin/sftp-bind-jails.sh \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; #!/bin/bash HOMES_ROOT=\u0026#34;/mnt/data/homes\u0026#34; PUBLIC_ROOT=\u0026#34;/mnt/data2/Public\u0026#34; SFTP_ROOT=\u0026#34;/srv/sftp\u0026#34; for jail in \u0026#34;$SFTP_ROOT\u0026#34;/*/; do user=$(basename \u0026#34;$jail\u0026#34;) mkdir -p \u0026#34;${jail}home\u0026#34; \u0026#34;${jail}public\u0026#34; mountpoint -q \u0026#34;${jail}home\u0026#34; || mount --bind \u0026#34;${HOMES_ROOT}/${user}\u0026#34; \u0026#34;${jail}home\u0026#34; mountpoint -q \u0026#34;${jail}public\u0026#34; || mount --bind \u0026#34;$PUBLIC_ROOT\u0026#34; \u0026#34;${jail}public\u0026#34; done EOF sudo chmod +x /usr/local/bin/sftp-bind-jails.sh sudo tee /etc/systemd/system/sftp-bind-jails.service \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; [Unit] Description=Bind-mount all my SFTP chroot jails After=mnt-data-homes.mount mnt-data2.mount [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/bin/sftp-bind-jails.sh [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now sftp-bind-jails.service What I\u0026rsquo;d tell past-me If you\u0026rsquo;re mounting something via NFS, check whether you\u0026rsquo;re about to re-export it. If yes, stop, use Samba instead. A local disk needs its own mountpoint. Not near anything else. getfacl showing the right answer doesn\u0026rsquo;t mean the underlying protocol is going to respect it. Check the raw UID. Anything serving through more than one protocol needs its SELinux context checked before you assume it\u0026rsquo;s a permissions bug elsewhere. Never trust a live mount --bind to survive a reboot. If it matters, it goes in a config file or a service, not just a command you ran once. None of this was smart engineering. It was mostly stubbornness and a lot of journalctl at hours I\u0026rsquo;m not proud of. But the box works now, and I understand every single piece of why, which is worth more than if it had just worked first try.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/samba-sftp-lessons/","summary":"\u003ch2 id=\"the-idea-was-simple\"\u003eThe idea was simple\u003c/h2\u003e\n\u003cp\u003eAll I wanted was for my T620 to mount the QNAP shares, retire the QNAP as\nthe \u0026ldquo;brain,\u0026rdquo; and have the T620 do everything \u0026hellip; Samba, SFTP, the lot \u0026hellip;\nwhile the QNAP just sat there as dumb storage in the background. Simple\nnetworking. I have never been more wrong about something taking one evening.\u003c/p\u003e\n\u003ch2 id=\"boot-failure-1-the-nfs-ordering-cycle\"\u003eBoot failure #1: the NFS ordering cycle\u003c/h2\u003e\n\u003cp\u003eFirst attempt, I set up NFS mounts from the QNAP onto the T620, then\nre-exported those same paths back out via NFS so other machines could\nreach them through the T620. Seemed logical \u0026hellip; one machine, one point of\naccess.\u003c/p\u003e","title":"Hard-Won Lessons Setting Up Samba + SFTP on a NAS"},{"content":"Two boxes, two philosophies I run Fedora Server on my T620 NAS and NixOS with KDE Plasma on my daily driver desktop. Not a lab experiment, not \u0026ldquo;let me try this for a week and write a hot take\u0026rdquo; \u0026hellip; these are both machines I actually depend on every day, which means whatever broke, I had to actually fix, not just note down and move on.\nFedora: familiar, fast to fix, one config file at a time Fedora Server feels like driving a car you already know how to drive. Something breaks, you find the config file, you edit it, you restart the service. dnf, systemctl, /etc/. No new mental model required.\nWhere it earns its keep is the small, boring stuff. Wrong I/O scheduler for a NAS workload? One udev rule, done, persists across reboots without having to think about it again:\nsudo tee /etc/udev/rules.d/60-ioscheduler.rules \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; ACTION==\u0026#34;add|change\u0026#34;, KERNEL==\u0026#34;sda\u0026#34;, ATTR{queue/scheduler}=\u0026#34;mq-deadline\u0026#34; EOF sudo udevadm control --reload-rules sudo udevadm trigger --subsystem-match=block SELinux context wrong on a directory that needs to serve two different services? Same pattern every time, no surprises:\nsudo semanage fcontext -a -t public_content_rw_t \u0026#34;/some/path(/.*)?\u0026#34; sudo restorecon -Rv /some/path The downside only shows up later. Six months from now, if that box needs rebuilding, I have to remember every single one of these one-off tweaks myself, or dig through my own notes and hope I wrote them down properly. Fedora doesn\u0026rsquo;t know what I changed. Only I do.\nNixOS: everything lives in one place, whether you like it or not NixOS flips that completely. Instead of scattered /etc/ edits, every change is a block of config in a .nix file, and the entire system \u0026hellip; services, packages, users, systemd units \u0026hellip; gets rebuilt from that description every time.\nThat sounds academic until something actually breaks repeatedly and you get sick of fixing it by hand. My desktop kept losing WiFi after suspend. Instead of running systemctl restart NetworkManager every single time I woke the machine, I wrote it once, as a real system service tied to the suspend target:\nsystemd.services.nm-resume-fix = { description = \u0026#34;Restart FUCKING NetworkManager after resume\u0026#34;; after = [ \u0026#34;suspend.target\u0026#34; \u0026#34;hibernate.target\u0026#34; \u0026#34;hybrid-sleep.target\u0026#34; ]; wantedBy = [ \u0026#34;suspend.target\u0026#34; \u0026#34;hibernate.target\u0026#34; \u0026#34;hybrid-sleep.target\u0026#34; ]; serviceConfig = { Type = \u0026#34;oneshot\u0026#34;; ExecStart = \u0026#34;${pkgs.bash}/bin/bash -c \u0026#39;sleep 2 \u0026amp;\u0026amp; systemctl restart NetworkManager\u0026#39;\u0026#34;; }; }; Same story when the screen kept coming back dim after every resume \u0026hellip; not an actual brightness problem, KWin\u0026rsquo;s own render state getting stuck. Once I found the right fix, it went into the config as its own service instead of a command I\u0026rsquo;d have to remember to run manually forever:\nsystemd.services.kwin-resume-fix = { description = \u0026#34;Reconfigure the FUCKING KWin after resume\u0026#34;; after = [ \u0026#34;suspend.target\u0026#34; \u0026#34;hibernate.target\u0026#34; \u0026#34;hybrid-sleep.target\u0026#34; ]; wantedBy = [ \u0026#34;suspend.target\u0026#34; \u0026#34;hibernate.target\u0026#34; \u0026#34;hybrid-sleep.target\u0026#34; ]; serviceConfig = { Type = \u0026#34;oneshot\u0026#34;; ExecStart = \u0026#34;${pkgs.bash}/bin/bash -c \u0026#39;sleep 2 \u0026amp;\u0026amp; qdbus org.kde.KWin /KWin reconfigure\u0026#39;\u0026#34;; }; }; sudo nixos-rebuild switch That\u0026rsquo;s the whole appeal. Every fix I\u0026rsquo;ve ever needed on this desktop is sitting in version-controlled files right now. If this machine died tomorrow and I rebuilt from the same config on new hardware, every one of these annoyances would already be solved before I even logged in.\nWhere NixOS actually costs you time It\u0026rsquo;s not free. The declarative model means even small things go through an extra layer of indirection \u0026hellip; you don\u0026rsquo;t just systemctl restart a user service and move on, you have to know whether it\u0026rsquo;s a system service or a user service, because they live in genuinely different places in the config:\nsystemd.services.something = { ... }; # system-level systemd.user.services.something = { ... }; # session-level, different key entirely Get that wrong and the whole rebuild fails with an option-doesn\u0026rsquo;t-exist error that doesn\u0026rsquo;t always point at the actual mistake clearly. I\u0026rsquo;ve lost real time to exactly that \u0026hellip; a service nested one level too deep in the wrong block, config builds fine syntactically, just silently means nothing you wrote actually does what you think.\nDebugging in the moment is also just slower. Fedora, I edit a file and restart a service in ten seconds. NixOS, every change is a full nixos-rebuild switch \u0026hellip; usually quick, but it\u0026rsquo;s still a build step in the loop every single time, even for a one-line tweak.\nThe actual verdict For the NAS \u0026hellip; Fedora, no contest. It\u0026rsquo;s a server that mostly just needs to run and rarely gets touched once it\u0026rsquo;s stable. I don\u0026rsquo;t need reproducible infra for a box I\u0026rsquo;m not planning to nuke and rebuild every month. Quick, direct config edits are the right tool there.\nFor the desktop I actually sit at every day, making changes to constantly \u0026hellip; NixOS wins, specifically because I keep hitting the same category of annoying, recurring bugs (resume glitches, one-off tweaks I\u0026rsquo;d otherwise forget). Every fix becomes permanent the moment it\u0026rsquo;s in the config. I\u0026rsquo;m not fixing the same problem twice.\nNeither one is \u0026ldquo;better\u0026rdquo; in the abstract. It\u0026rsquo;s genuinely about how often you touch the box and whether you\u0026rsquo;d rather remember your fixes yourself, or have the system remember them for you.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/nixos-vs-fedora/","summary":"\u003ch2 id=\"two-boxes-two-philosophies\"\u003eTwo boxes, two philosophies\u003c/h2\u003e\n\u003cp\u003eI run Fedora Server on my T620 NAS and NixOS with KDE Plasma on my daily\ndriver desktop. Not a lab experiment, not \u0026ldquo;let me try this for a week and\nwrite a hot take\u0026rdquo; \u0026hellip; these are both machines I actually depend on every\nday, which means whatever broke, I had to actually fix, not just note\ndown and move on.\u003c/p\u003e\n\u003ch2 id=\"fedora-familiar-fast-to-fix-one-config-file-at-a-time\"\u003eFedora: familiar, fast to fix, one config file at a time\u003c/h2\u003e\n\u003cp\u003eFedora Server feels like driving a car you already know how to drive.\nSomething breaks, you find the config file, you edit it, you restart the\nservice. \u003ccode\u003ednf\u003c/code\u003e, \u003ccode\u003esystemctl\u003c/code\u003e, \u003ccode\u003e/etc/\u003c/code\u003e. No new mental model required.\u003c/p\u003e","title":"NixOS vs Fedora: Which One Actually Wins for me"},{"content":"This is my first post on JackSparrow2.\n","permalink":"https://jacksparrow2.tail9e758e.ts.net/posts/first-post/","summary":"\u003cp\u003eThis is my first post on JackSparrow2.\u003c/p\u003e","title":"First Post"},{"content":"","permalink":"https://jacksparrow2.tail9e758e.ts.net/newsletter/","summary":"","title":"Newsletter"}]